Merge mozilla-central to inbound. a=merge CLOSED TREE
authorNoemi Erli <nerli@mozilla.com>
Wed, 22 Aug 2018 19:32:56 +0300
changeset 481181 ab8276b5924e4ade5daec800cefc702f72cef34c
parent 481164 6fef7bc080783ae8df267bb905bb301c05e86cbd (current diff)
parent 481180 d6e4d3e69d4c8331cfa35c318b616ed390d3538d (diff)
child 481182 e42366ac19e98683f5680ebaadeb2588927f3289
push id232
push userfmarier@mozilla.com
push dateWed, 05 Sep 2018 20:45:54 +0000
reviewersmerge
milestone63.0a1
Merge mozilla-central to inbound. a=merge CLOSED TREE
browser/base/content/browser-contentblocking.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1497,16 +1497,19 @@ pref("browser.contentblocking.ui.enabled
 pref("browser.contentblocking.cookies-site-data.ui.reject-trackers.enabled", false);
 #endif
 #ifdef NIGHTLY_BUILD
 pref("browser.contentblocking.reportBreakage.enabled", true);
 #else
 pref("browser.contentblocking.reportBreakage.enabled", false);
 #endif
 pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
+// Content Blocking has a separate pref for the intro count, since the former TP intro
+// was updated when we introduced content blocking and we want people to see it again.
+pref("browser.contentblocking.introCount", 0);
 
 pref("privacy.trackingprotection.introCount", 0);
 pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/");
 
 // Always enable newtab segregation using containers
 pref("privacy.usercontext.about_newtab_segregation.enabled", true);
 // Enable Contextual Identity Containers
 #ifdef NIGHTLY_BUILD
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -129,21 +129,27 @@ var TrackingProtection = {
 var ContentBlocking = {
   // If the user ignores the doorhanger, we stop showing it after some time.
   MAX_INTROS: 20,
   PREF_ENABLED: "browser.contentblocking.enabled",
   PREF_UI_ENABLED: "browser.contentblocking.ui.enabled",
   PREF_ANIMATIONS_ENABLED: "toolkit.cosmeticAnimations.enabled",
   PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.reportBreakage.enabled",
   PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
+  PREF_INTRO_COUNT_CB: "browser.contentblocking.introCount",
+  PREF_INTRO_COUNT_TP: "privacy.trackingprotection.introCount",
   content: null,
   icon: null,
   activeTooltipText: null,
   disabledTooltipText: null,
 
+  get prefIntroCount() {
+    return this.contentBlockingUIEnabled ? this.PREF_INTRO_COUNT_CB : this.PREF_INTRO_COUNT_TP;
+  },
+
   get appMenuLabel() {
     delete this.appMenuLabel;
     return this.appMenuLabel = document.getElementById("appMenu-tp-label");
   },
 
   get appMenuButton() {
     delete this.appMenuButton;
     return this.appMenuButton = document.getElementById("appMenu-tp-toggle");
@@ -424,24 +430,21 @@ var ContentBlocking = {
     this.iconBox.toggleAttribute("active", active);
     this.iconBox.toggleAttribute("hasException", this.enabled && hasException);
 
     if (isSimulated) {
       this.iconBox.removeAttribute("animate");
     } else if (active && webProgress.isTopLevel) {
       this.iconBox.setAttribute("animate", "true");
 
-      // Open the tracking protection introduction panel, if applicable.
-      if (TrackingProtection.enabledGlobally) {
-        let introCount = Services.prefs.getIntPref("privacy.trackingprotection.introCount");
-        if (introCount < this.MAX_INTROS) {
-          Services.prefs.setIntPref("privacy.trackingprotection.introCount", ++introCount);
-          Services.prefs.savePrefFile(null);
-          this.showIntroPanel();
-        }
+      let introCount = Services.prefs.getIntPref(this.prefIntroCount);
+      if (introCount < this.MAX_INTROS) {
+        Services.prefs.setIntPref(this.prefIntroCount, ++introCount);
+        Services.prefs.savePrefFile(null);
+        this.showIntroPanel();
       }
     }
 
     if (hasException) {
       this.iconBox.setAttribute("tooltiptext", this.disabledTooltipText);
       this.shieldHistogramAdd(1);
     } else if (active) {
       this.iconBox.setAttribute("tooltiptext", this.activeTooltipText);
@@ -488,36 +491,59 @@ var ContentBlocking = {
 
     // Telemetry for enable protection.
     this.eventsHistogramAdd(2);
 
     this.hideIdentityPopupAndReload();
   },
 
   dontShowIntroPanelAgain() {
-    // This function may be called in private windows, but it does not change
-    // any preference unless Tracking Protection is enabled globally.
-    if (TrackingProtection.enabledGlobally) {
-      Services.prefs.setIntPref("privacy.trackingprotection.introCount",
-                                this.MAX_INTROS);
+    if (!PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
+      Services.prefs.setIntPref(this.prefIntroCount, this.MAX_INTROS);
       Services.prefs.savePrefFile(null);
     }
   },
 
   async showIntroPanel() {
     let brandBundle = document.getElementById("bundle_brand");
     let brandShortName = brandBundle.getString("brandShortName");
 
+    let introTitle;
+    let introDescription;
+    // This will be sent to the onboarding website to let them know which
+    // UI variation we're showing.
+    let variation;
+
+    if (this.contentBlockingUIEnabled) {
+      introTitle = gNavigatorBundle.getFormattedString("contentBlocking.intro.title",
+                                                       [brandShortName]);
+      // We show a different UI tour variation for users that already have TP
+      // enabled globally.
+      if (TrackingProtection.enabledGlobally) {
+        introDescription = gNavigatorBundle.getString("contentBlocking.intro.v2.description");
+        variation = 2;
+      } else {
+        introDescription = gNavigatorBundle.getFormattedString("contentBlocking.intro.v1.description",
+                                                               [brandShortName]);
+        variation = 1;
+      }
+    } else {
+      introTitle = gNavigatorBundle.getString("trackingProtection.intro.title");
+      introDescription = gNavigatorBundle.getFormattedString("trackingProtection.intro.description2",
+                                                             [brandShortName]);
+      variation = 0;
+    }
+
     let openStep2 = () => {
       // When the user proceeds in the tour, adjust the counter to indicate that
       // the user doesn't need to see the intro anymore.
       this.dontShowIntroPanelAgain();
 
       let nextURL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
-                    "?step=2&newtab=true";
+                    `?step=2&newtab=true&variation=${variation}`;
       switchToTabHavingURI(nextURL, true, {
         // Ignore the fragment in case the intro is shown on the tour page
         // (e.g. if the user manually visited the tour or clicked the link from
         // about:privatebrowsing) so we can avoid a reload.
         ignoreFragment: "whenComparingAndReplace",
         triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
       });
     };
@@ -531,16 +557,12 @@ var ContentBlocking = {
         callback: openStep2,
         label: gNavigatorBundle.getString("trackingProtection.intro.nextButton.label"),
         style: "primary",
       },
     ];
 
     let panelTarget = await UITour.getTarget(window, "trackingProtection");
     UITour.initForBrowser(gBrowser.selectedBrowser, window);
-    UITour.showInfo(window, panelTarget,
-                    gNavigatorBundle.getString("trackingProtection.intro.title"),
-                    gNavigatorBundle.getFormattedString("trackingProtection.intro.description2",
-                                                        [brandShortName]),
-                    undefined, buttons,
+    UITour.showInfo(window, panelTarget, introTitle, introDescription, undefined, buttons,
                     { closeButtonCallback: () => this.dontShowIntroPanelAgain() });
   },
 };
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1533,20 +1533,31 @@ var BookmarkingUI = {
   updateBookmarkPageMenuItem: function BUI_updateBookmarkPageMenuItem(forceReset) {
     if (!this.stringbundleset) {
       // We are loaded in a non-browser context, like the sidebar.
       return;
     }
     let isStarred = !forceReset && this._itemGuids.size > 0;
     let label = this.stringbundleset.getAttribute(
       isStarred ? "string-editthisbookmark" : "string-bookmarkthispage");
+
+    let panelMenuToolbarButton =
+      document.getElementById("panelMenuBookmarkThisPage");
+    if (!panelMenuToolbarButton) {
+      // We don't have the star UI or context menu (e.g. we're the hidden
+      // window). So we just set the bookmarks menu item label and exit.
+      document.getElementById("menu_bookmarkThisPage")
+              .setAttribute("label", label);
+      return;
+    }
+
     for (let element of [
       document.getElementById("menu_bookmarkThisPage"),
       document.getElementById("context-bookmarkpage"),
-      document.getElementById("panelMenuBookmarkThisPage"),
+      panelMenuToolbarButton,
     ]) {
       element.setAttribute("label", label);
     }
 
     // Update the title and the starred state for the page action panel.
     PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK)
                .setTitle(label, window);
     this._updateStar();
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -18,16 +18,18 @@ skip-if = (verify && debug)
 [browser_bookmark_change_location.js]
 [browser_bookmark_folder_moveability.js]
 [browser_bookmark_private_window.js]
 skip-if = (verify && debug && (os == 'win' || os == 'mac'))
 [browser_bookmark_remove_tags.js]
 [browser_bookmarklet_windowOpen.js]
 support-files =
   bookmarklet_windowOpen_dummy.html
+[browser_bookmarkMenu_hiddenWindow.js]
+skip-if = os != 'mac' # Mac-only functionality
 [browser_bookmarks_change_title.js]
 [browser_bookmarks_sidebar_search.js]
 support-files =
   pageopeningwindow.html
 [browser_bookmarkProperties_addFolderDefaultButton.js]
 [browser_bookmarkProperties_addKeywordForThisSearch.js]
 skip-if = (verify && debug)
 [browser_bookmarkProperties_addLivemark.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function setup() {
+  await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    url: "http://example.com/",
+    title: "Test1",
+  });
+
+  registerCleanupFunction(async () => {
+    await PlacesUtils.bookmarks.eraseEverything();
+  });
+});
+
+add_task(async function test_menu_in_hidden_window() {
+  let hwDoc = Services.appShell.hiddenDOMWindow.document;
+  let bmPopup = hwDoc.getElementById("bookmarksMenuPopup");
+  var popupEvent = hwDoc.createEvent("MouseEvent");
+  popupEvent.initMouseEvent("popupshowing", true, true, Services.appShell.hiddenDOMWindow, 0,
+                            0, 0, 0, 0, false, false, false, false,
+                            0, null);
+  bmPopup.dispatchEvent(popupEvent);
+
+  let testMenuitem = [...bmPopup.children].find(
+    node => node.getAttribute("label") == "Test1");
+  Assert.ok(testMenuitem,
+    "Should have found the test bookmark in the hidden window bookmark menu");
+});
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -3,16 +3,17 @@ support-files =
   head.js
   image.png
   uitour.html
   ../UITour-lib.js
 
 [browser_backgroundTab.js]
 [browser_closeTab.js]
 skip-if = (verify && !debug && (os == 'linux'))
+[browser_contentBlocking.js]
 [browser_fxa.js]
 skip-if = debug || asan # updateUI leaks
 [browser_no_tabs.js]
 [browser_openPreferences.js]
 [browser_openSearchPanel.js]
 skip-if = true # Bug 1113038 - Intermittent "Popup was opened"
 [browser_trackingProtection.js]
 skip-if = os == "linux" # Intermittent NS_ERROR_NOT_AVAILABLE [nsIUrlClassifierDBService.beginUpdate]
copy from browser/components/uitour/test/browser_trackingProtection.js
copy to browser/components/uitour/test/browser_contentBlocking.js
--- a/browser/components/uitour/test/browser_trackingProtection.js
+++ b/browser/components/uitour/test/browser_contentBlocking.js
@@ -1,65 +1,64 @@
 "use strict";
 
-const PREF_INTRO_COUNT = "privacy.trackingprotection.introCount";
-const PREF_CB_ENABLED = "browser.contentblocking.enabled";
+const PREF_INTRO_COUNT = "browser.contentblocking.introCount";
+const PREF_CB_UI_ENABLED = "browser.contentblocking.ui.enabled";
 const PREF_TP_ENABLED = "privacy.trackingprotection.enabled";
+const PREF_FB_ENABLED = "browser.fastblock.enabled";
+const PREF_FB_TIMEOUT = "browser.fastblock.timeout";
 const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
 const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
 const TOOLTIP_PANEL = document.getElementById("UITourTooltip");
 const TOOLTIP_ANCHOR = document.getElementById("tracking-protection-icon-animatable-box");
 
 var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
 
 registerCleanupFunction(function() {
   UrlClassifierTestUtils.cleanupTestTrackers();
-  Services.prefs.clearUserPref(PREF_CB_ENABLED);
+  Services.prefs.clearUserPref(PREF_CB_UI_ENABLED);
   Services.prefs.clearUserPref(PREF_TP_ENABLED);
+  Services.prefs.clearUserPref(PREF_FB_ENABLED);
+  Services.prefs.clearUserPref(PREF_FB_TIMEOUT);
   Services.prefs.clearUserPref(PREF_INTRO_COUNT);
 });
 
 function allowOneIntro() {
   Services.prefs.setIntPref(PREF_INTRO_COUNT, window.ContentBlocking.MAX_INTROS - 1);
 }
 
 add_task(async function setup_test() {
-  Services.prefs.setBoolPref(PREF_CB_ENABLED, true);
+  Services.prefs.setBoolPref(PREF_CB_UI_ENABLED, true);
   Services.prefs.setBoolPref(PREF_TP_ENABLED, true);
   await UrlClassifierTestUtils.addTestTrackers();
 });
 
 add_task(async function test_benignPage() {
   info("Load a test page not containing tracking elements");
   allowOneIntro();
-  await BrowserTestUtils.withNewTab({gBrowser, url: BENIGN_PAGE}, async function() {
-    await waitForConditionPromise(() => {
+  await BrowserTestUtils.withNewTab(BENIGN_PAGE, async function() {
+    await Assert.rejects(waitForConditionPromise(() => {
       return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
-    }, "Info panel shouldn't appear on a benign page").
-      then(() => ok(false, "Info panel shouldn't appear"),
-           () => {
-             ok(true, "Info panel didn't appear on a benign page");
-           });
-
+    }, "timeout"), /timeout/, "Info panel shouldn't appear on a benign page");
   });
 });
 
-add_task(async function test_trackingPages() {
+add_task(async function test_tracking() {
   info("Load a test page containing tracking elements");
   allowOneIntro();
   await BrowserTestUtils.withNewTab({gBrowser, url: TRACKING_PAGE}, async function() {
     await new Promise((resolve, reject) => {
       waitForPopupAtAnchor(TOOLTIP_PANEL, TOOLTIP_ANCHOR, resolve,
                            "Intro panel should appear");
     });
 
     is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
 
     let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
-                   "?step=2&newtab=true";
+                   "?step=2&newtab=true&variation=2";
     let buttons = document.getElementById("UITourTooltipButtons");
 
     info("Click the step text and nothing should happen");
     let tabCount = gBrowser.tabs.length;
     await EventUtils.synthesizeMouseAtCenter(buttons.children[0], {});
     is(gBrowser.tabs.length, tabCount, "Same number of tabs should be open");
 
     info("Resetting count to test that viewing the tour prevents future panels");
@@ -74,19 +73,64 @@ add_task(async function test_trackingPag
        "Check intro count is at the max after opening step 2");
     is(gBrowser.tabs.length, tabCount + 1, "Tour step 2 tab opened");
     await panelHiddenPromise;
     ok(true, "Panel hid when the button was clicked");
     BrowserTestUtils.removeTab(tab);
   });
 
   info("Open another tracking page and make sure we don't show the panel again");
-  await BrowserTestUtils.withNewTab({gBrowser, url: TRACKING_PAGE}, async function() {
-    await waitForConditionPromise(() => {
+  await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
+    await Assert.rejects(waitForConditionPromise(() => {
       return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
-    }, "Info panel shouldn't appear more than MAX_INTROS").
-      then(() => ok(false, "Info panel shouldn't appear again"),
-           () => {
-             ok(true, "Info panel didn't appear more than MAX_INTROS on tracking pages");
-           });
-
+    }, "timeout"), /timeout/, "Info panel shouldn't appear more than MAX_INTROS");
   });
 });
+
+add_task(async function test_fastBlock() {
+  Services.prefs.clearUserPref(PREF_INTRO_COUNT);
+
+  Services.prefs.setBoolPref(PREF_TP_ENABLED, false);
+  Services.prefs.setBoolPref(PREF_FB_ENABLED, true);
+  Services.prefs.setIntPref(PREF_FB_TIMEOUT, 0);
+
+  info("Load a test page containing tracking elements for FastBlock");
+  allowOneIntro();
+  await BrowserTestUtils.withNewTab({gBrowser, url: TRACKING_PAGE}, async function() {
+    await new Promise((resolve, reject) => {
+      waitForPopupAtAnchor(TOOLTIP_PANEL, TOOLTIP_ANCHOR, resolve,
+                           "Intro panel should appear");
+    });
+
+    is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
+
+    let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
+                   "?step=2&newtab=true&variation=1";
+    let buttons = document.getElementById("UITourTooltipButtons");
+
+    info("Click the step text and nothing should happen");
+    let tabCount = gBrowser.tabs.length;
+    await EventUtils.synthesizeMouseAtCenter(buttons.children[0], {});
+    is(gBrowser.tabs.length, tabCount, "Same number of tabs should be open");
+
+    info("Resetting count to test that viewing the tour prevents future panels");
+    allowOneIntro();
+
+    let panelHiddenPromise = promisePanelElementHidden(window, TOOLTIP_PANEL);
+    let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, step2URL);
+    info("Clicking the main button");
+    EventUtils.synthesizeMouseAtCenter(buttons.children[1], {});
+    let tab = await tabPromise;
+    is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS,
+       "Check intro count is at the max after opening step 2");
+    is(gBrowser.tabs.length, tabCount + 1, "Tour step 2 tab opened");
+    await panelHiddenPromise;
+    ok(true, "Panel hid when the button was clicked");
+    BrowserTestUtils.removeTab(tab);
+  });
+
+  info("Open another tracking page and make sure we don't show the panel again");
+  await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
+    await Assert.rejects(waitForConditionPromise(() => {
+      return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
+    }, "timeout"), /timeout/, "Info panel shouldn't appear more than MAX_INTROS");
+  });
+});
--- a/browser/components/uitour/test/browser_trackingProtection.js
+++ b/browser/components/uitour/test/browser_trackingProtection.js
@@ -1,33 +1,33 @@
 "use strict";
 
 const PREF_INTRO_COUNT = "privacy.trackingprotection.introCount";
-const PREF_CB_ENABLED = "browser.contentblocking.enabled";
+const PREF_CB_UI_ENABLED = "browser.contentblocking.ui.enabled";
 const PREF_TP_ENABLED = "privacy.trackingprotection.enabled";
 const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
 const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
 const TOOLTIP_PANEL = document.getElementById("UITourTooltip");
 const TOOLTIP_ANCHOR = document.getElementById("tracking-protection-icon-animatable-box");
 
 var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
 
 registerCleanupFunction(function() {
   UrlClassifierTestUtils.cleanupTestTrackers();
-  Services.prefs.clearUserPref(PREF_CB_ENABLED);
+  Services.prefs.clearUserPref(PREF_CB_UI_ENABLED);
   Services.prefs.clearUserPref(PREF_TP_ENABLED);
   Services.prefs.clearUserPref(PREF_INTRO_COUNT);
 });
 
 function allowOneIntro() {
   Services.prefs.setIntPref(PREF_INTRO_COUNT, window.ContentBlocking.MAX_INTROS - 1);
 }
 
 add_task(async function setup_test() {
-  Services.prefs.setBoolPref(PREF_CB_ENABLED, true);
+  Services.prefs.setBoolPref(PREF_CB_UI_ENABLED, false);
   Services.prefs.setBoolPref(PREF_TP_ENABLED, true);
   await UrlClassifierTestUtils.addTestTrackers();
 });
 
 add_task(async function test_benignPage() {
   info("Load a test page not containing tracking elements");
   allowOneIntro();
   await BrowserTestUtils.withNewTab({gBrowser, url: BENIGN_PAGE}, async function() {
@@ -49,17 +49,17 @@ add_task(async function test_trackingPag
     await new Promise((resolve, reject) => {
       waitForPopupAtAnchor(TOOLTIP_PANEL, TOOLTIP_ANCHOR, resolve,
                            "Intro panel should appear");
     });
 
     is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
 
     let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
-                   "?step=2&newtab=true";
+                   "?step=2&newtab=true&variation=0";
     let buttons = document.getElementById("UITourTooltipButtons");
 
     info("Click the step text and nothing should happen");
     let tabCount = gBrowser.tabs.length;
     await EventUtils.synthesizeMouseAtCenter(buttons.children[0], {});
     is(gBrowser.tabs.length, tabCount, "Same number of tabs should be open");
 
     info("Resetting count to test that viewing the tour prevents future panels");
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -534,16 +534,22 @@ trackingProtection.intro.title=How Track
 # LOCALIZATION NOTE (trackingProtection.intro.description2):
 # %S is brandShortName. This string should match the one from Step 1 of the tour
 # when it starts from the button shown when a new private window is opened.
 trackingProtection.intro.description2=When you see the shield, %S is blocking some parts of the page that could track your browsing activity.
 # LOCALIZATION NOTE (trackingProtection.intro.step1of3): Indicates that the intro panel is step one of three in a tour.
 trackingProtection.intro.step1of3=1 of 3
 trackingProtection.intro.nextButton.label=Next
 
+# LOCALIZATION NOTE (contentBlocking.intro.title): %S is brandShortName.
+contentBlocking.intro.title=New in %S: Content Blocking
+# LOCALIZATION NOTE (contentBlocking.v1.intro.description): %S is brandShortName.
+contentBlocking.intro.v1.description=When you see the shield, %S is blocking parts of the page that can slow your browsing or track you online.
+contentBlocking.intro.v2.description=The privacy benefits of Tracking Protection are now just one part of content blocking. When you see the shield, content blocking is on.
+
 trackingProtection.toggle.enable.tooltip=Enable Tracking Protection
 trackingProtection.toggle.disable.tooltip=Disable Tracking Protection
 trackingProtection.toggle.enable.pbmode.tooltip=Enable Tracking Protection in Private Browsing
 trackingProtection.toggle.disable.pbmode.tooltip=Disable Tracking Protection in Private Browsing
 
 trackingProtection.icon.activeTooltip=Tracking attempts blocked
 trackingProtection.icon.disabledTooltip=Tracking content detected
 
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -626,17 +626,17 @@ MarkupView.prototype = {
     return !isHighlight && reason && !unwantedReasons.includes(reason);
   },
 
   /**
    * React to new-node-front selection events.
    * Highlights the node if needed, and make sure it is shown and selected in
    * the view.
    */
-  _onNewSelection: function() {
+  _onNewSelection: function(nodeFront, reason) {
     const selection = this.inspector.selection;
 
     if (this.htmlEditor) {
       this.htmlEditor.hide();
     }
     if (this._isContainerSelected(this._hoveredContainer)) {
       this._hoveredContainer.hovered = false;
       this._hoveredContainer = null;
@@ -651,30 +651,32 @@ MarkupView.prototype = {
     let onShowBoxModel;
 
     // Highlight the element briefly if needed.
     if (this._shouldNewSelectionBeHighlighted()) {
       onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
     }
 
     const slotted = selection.isSlotted();
-    const onShow = this.showNode(selection.nodeFront, { slotted }).then(() => {
-      // We could be destroyed by now.
-      if (this._destroyer) {
-        return promise.reject("markupview destroyed");
-      }
+    const smoothScroll = reason === "reveal-from-slot";
+    const onShow = this.showNode(selection.nodeFront, { slotted, smoothScroll })
+      .then(() => {
+        // We could be destroyed by now.
+        if (this._destroyer) {
+          return promise.reject("markupview destroyed");
+        }
 
-      // Mark the node as selected.
-      const container = this.getContainer(selection.nodeFront, slotted);
-      this._markContainerAsSelected(container);
+        // Mark the node as selected.
+        const container = this.getContainer(selection.nodeFront, slotted);
+        this._markContainerAsSelected(container);
 
-      // Make sure the new selection is navigated to.
-      this.maybeNavigateToNewSelection();
-      return undefined;
-    }).catch(this._handleRejectionIfNotDestroyed);
+        // Make sure the new selection is navigated to.
+        this.maybeNavigateToNewSelection();
+        return undefined;
+      }).catch(this._handleRejectionIfNotDestroyed);
 
     promise.all([onShowBoxModel, onShow]).then(done);
   },
 
   /**
    * Maybe make selected the current node selection's MarkupContainer depending
    * on why the current node got selected.
    */
@@ -1198,31 +1200,31 @@ MarkupView.prototype = {
       container.flashMutation();
     }
   },
 
   /**
    * Make sure the given node's parents are expanded and the
    * node is scrolled on to screen.
    */
-  showNode: function(node, {centered = true, slotted} = {}) {
+  showNode: function(node, {centered = true, slotted, smoothScroll = false} = {}) {
     if (slotted && !this.hasContainer(node, slotted)) {
       throw new Error("Tried to show a slotted node not previously imported");
     } else {
       this._ensureNodeImported(node);
     }
 
     return this._waitForChildren().then(() => {
       if (this._destroyer) {
         return promise.reject("markupview destroyed");
       }
       return this._ensureVisible(node);
     }).then(() => {
       const container = this.getContainer(node, slotted);
-      scrollIntoViewIfNeeded(container.editor.elt, centered);
+      scrollIntoViewIfNeeded(container.editor.elt, centered, smoothScroll);
     }, this._handleRejectionIfNotDestroyed);
   },
 
   _ensureNodeImported: function(node) {
     let parent = node;
 
     this.importNode(node);
 
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal_scroll.js
@@ -59,27 +59,29 @@ add_task(async function() {
   const realElement = realContainer.elt;
 
   info("Click on the reveal link");
   await clickOnRevealLink(inspector, slottedContainer);
   // "new-node-front" will also trigger the scroll, so make sure we are testing after
   // the scroll was performed.
   await waitUntil(() => isScrolledOut(slottedElement));
   is(isScrolledOut(slottedElement), true, "slotted element is scrolled out");
+  await waitUntil(() => !isScrolledOut(realElement));
   is(isScrolledOut(realElement), false, "real element is not scrolled out");
 
   info("Scroll back to see the slotted element");
   slottedElement.scrollIntoView();
   is(isScrolledOut(slottedElement), false, "slotted element is not scrolled out");
   is(isScrolledOut(realElement), true, "real element is scrolled out");
 
   info("Click on the reveal link again");
   await clickOnRevealLink(inspector, slottedContainer);
   await waitUntil(() => isScrolledOut(slottedElement));
   is(isScrolledOut(slottedElement), true, "slotted element is scrolled out");
+  await waitUntil(() => !isScrolledOut(realElement));
   is(isScrolledOut(realElement), false, "real element is not scrolled out");
 });
 
 function isScrolledOut(element) {
   const win = element.ownerGlobal;
   const rect = element.getBoundingClientRect();
   return rect.top < 0 || (rect.top + rect.height) > win.innerHeight;
 }
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -346,17 +346,17 @@ class FirefoxDataProvider {
    */
   onNetworkEventUpdate(data) {
     const { packet, networkInfo } = data;
     const { actor } = networkInfo;
     const { updateType } = packet;
 
     switch (updateType) {
       case "securityInfo":
-        this.pushRequestToQueue(actor, { securityState: networkInfo.securityInfo });
+        this.pushRequestToQueue(actor, { securityState: networkInfo.securityState });
         break;
       case "responseStart":
         this.pushRequestToQueue(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -82,17 +82,17 @@ function requestsReducer(state = Request
 
       let request = requests.get(action.id);
       if (!request) {
         return state;
       }
 
       request = {
         ...request,
-        ...processNetworkUpdates(action.data),
+        ...processNetworkUpdates(action.data, request),
       };
       const requestEndTime = request.startedMillis +
         (request.eventTimings ? request.eventTimings.totalTime : 0);
 
       return {
         ...state,
         requests: mapSet(state.requests, action.id, request),
         lastEndedMillis: requestEndTime > lastEndedMillis ?
--- a/devtools/client/netmonitor/src/utils/request-utils.js
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -486,28 +486,31 @@ async function updateFormDataSections(pr
   }
 }
 
 /**
  * This helper function is used for additional processing of
  * incoming network update packets. It's used by Network and
  * Console panel reducers.
  */
-function processNetworkUpdates(request = {}) {
+function processNetworkUpdates(update, request) {
   const result = {};
-  for (const [key, value] of Object.entries(request)) {
+  for (const [key, value] of Object.entries(update)) {
     if (UPDATE_PROPS.includes(key)) {
       result[key] = value;
 
       switch (key) {
         case "securityInfo":
           result.securityState = value.state;
           break;
+        case "securityState":
+          result.securityState = update.securityState || request.securityState;
+          break;
         case "totalTime":
-          result.totalTime = request.totalTime;
+          result.totalTime = update.totalTime;
           break;
         case "requestPostData":
           result.requestHeadersFromUploadStream = value.uploadHeaders;
           break;
       }
     }
   }
   return result;
--- a/devtools/client/shared/scroll.js
+++ b/devtools/client/shared/scroll.js
@@ -10,47 +10,60 @@ define(function(require, exports, module
    * Scroll the document so that the element "elem" appears in the viewport.
    *
    * @param {DOMNode} elem
    *        The element that needs to appear in the viewport.
    * @param {Boolean} centered
    *        true if you want it centered, false if you want it to appear on the
    *        top of the viewport. It is true by default, and that is usually what
    *        you want.
+   * @param {Boolean} smooth
+   *        true if you want the scroll to happen smoothly, instead of instantly.
+   *        It is false by default.
    */
-  function scrollIntoViewIfNeeded(elem, centered = true) {
+  function scrollIntoViewIfNeeded(elem, centered = true, smooth = false) {
     const win = elem.ownerDocument.defaultView;
     const clientRect = elem.getBoundingClientRect();
 
     // The following are always from the {top, bottom}
     // of the viewport, to the {top, …} of the box.
     // Think of them as geometrical vectors, it helps.
     // The origin is at the top left.
 
     const topToBottom = clientRect.bottom;
     const bottomToTop = clientRect.top - win.innerHeight;
     // We allow one translation on the y axis.
     let yAllowed = true;
 
+    // disable smooth scrolling when the user prefers reduced motion
+    const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
+    smooth = smooth && !reducedMotion;
+
+    const options = { behavior: smooth ? "smooth" : "auto" };
+
     // Whatever `centered` is, the behavior is the same if the box is
     // (even partially) visible.
     if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
-      win.scrollBy(0, topToBottom - elem.offsetHeight);
+      win.scrollBy(Object.assign(
+        {left: 0, top: topToBottom - elem.offsetHeight}, options));
       yAllowed = false;
     } else if ((bottomToTop < 0 || !centered) &&
               bottomToTop >= -elem.offsetHeight) {
-      win.scrollBy(0, bottomToTop + elem.offsetHeight);
+      win.scrollBy(Object.assign(
+        {left: 0, top: bottomToTop + elem.offsetHeight}, options));
+
       yAllowed = false;
     }
 
     // If we want it centered, and the box is completely hidden,
     // then we center it explicitly.
     if (centered) {
       if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
-        win.scroll(win.scrollX,
-                  win.scrollY + clientRect.top
-                  - (win.innerHeight - elem.offsetHeight) / 2);
+        const x = win.scrollX;
+        const y = win.scrollY + clientRect.top -
+          (win.innerHeight - elem.offsetHeight) / 2;
+        win.scroll(Object.assign({left: x, top: y}, options));
       }
     }
   }
   // Exports from this module
   module.exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded;
 });
--- a/devtools/client/shared/test/browser_layoutHelpers.js
+++ b/devtools/client/shared/test/browser_layoutHelpers.js
@@ -5,21 +5,21 @@
 
 // Tests that scrollIntoViewIfNeeded works properly.
 const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
 
 const TEST_URI = TEST_URI_ROOT + "doc_layoutHelpers.html";
 
 add_task(async function() {
   const [host, win] = await createHost("bottom", TEST_URI);
-  runTest(win);
+  await runTest(win);
   host.destroy();
 });
 
-function runTest(win) {
+async function runTest(win) {
   const some = win.document.getElementById("some");
 
   some.style.top = win.innerHeight + "px";
   some.style.left = win.innerWidth + "px";
   // The tests start with a black 2x2 pixels square below bottom right.
   // Do not resize the window during the tests.
 
   const xPos = Math.floor(win.innerWidth / 2);
@@ -85,9 +85,26 @@ function runTest(win) {
   // On the bottom edge.
   win.scroll(win.innerWidth / 2, 1);
   scrollIntoViewIfNeeded(some, false);
   is(win.scrollY, 2,
      "Element partially visible below should appear below " +
      "if parameter is false.");
   is(win.scrollX, xPos,
      "scrollX position has not changed.");
+
+  // Check smooth flag (scroll goes below the viewport)
+
+  info("Checking smooth flag");
+  is(win.matchMedia("(prefers-reduced-motion)").matches, false,
+    "Reduced motion is disabled");
+
+  const other = win.document.getElementById("other");
+  other.style.top = win.innerHeight + "px";
+  other.style.left = win.innerWidth + "px";
+  win.scroll(0, 0);
+
+  scrollIntoViewIfNeeded(other, false, true);
+  ok(win.scrollY < other.clientHeight,
+    "Window has not instantly scrolled to the final position");
+  await waitUntil(() => win.scrollY === other.clientHeight);
+  ok(true, "Window did finish scrolling");
 }
--- a/devtools/client/shared/test/doc_layoutHelpers.html
+++ b/devtools/client/shared/test/doc_layoutHelpers.html
@@ -8,17 +8,24 @@
     width: 300%;
   }
   div#some {
     position: absolute;
     background: black;
     width: 2px;
     height: 2px;
   }
+  div#other {
+    position: absolute;
+    background: red;
+    width: 2px;
+    height: 300px;
+  }
   iframe {
     position: absolute;
     width: 40px;
     height: 40px;
     border: 0;
   }
 </style>
 
 <div id=some></div>
+<div id="other"></div>
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1221,23 +1221,21 @@ class JSTerm extends Component {
       !completionText
       && this.autocompletePopup.isOpen
       && this.autocompletePopup.selectedItem
     ) {
       const {selectedItem} = this.autocompletePopup;
       completionText = selectedItem.label.substring(selectedItem.preLabel.length);
     }
 
-    if (!completionText) {
-      return false;
-    }
+    this.clearCompletion();
 
-    this.insertStringAtCursor(completionText);
-    this.clearCompletion();
-    return true;
+    if (completionText) {
+      this.insertStringAtCursor(completionText);
+    }
   }
 
   getInputValueBeforeCursor() {
     if (this.editor) {
       return this.editor.getDoc().getRange({line: 0, ch: 0}, this.editor.getCursor());
     }
 
     if (this.inputNode) {
--- a/devtools/client/webconsole/reducers/messages.js
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -309,17 +309,17 @@ function messages(state = MessageState()
       }
 
       return {
         ...state,
         networkMessagesUpdateById: {
           ...networkMessagesUpdateById,
           [action.id]: {
             ...request,
-            ...processNetworkUpdates(action.data),
+            ...processNetworkUpdates(action.data, request),
           }
         }
       };
     }
 
     case constants.REMOVED_ACTORS_CLEAR:
       return {
         ...state,
--- a/devtools/client/webconsole/test/fixtures/stubs/networkEvent.js
+++ b/devtools/client/webconsole/test/fixtures/stubs/networkEvent.js
@@ -25,17 +25,16 @@ stubPreparedMessages.set("GET request", 
   "response": {},
   "source": "network",
   "type": "log",
   "groupId": null,
   "timeStamp": 1487022056850,
   "indent": 0,
   "updates": [],
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "private": false,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
@@ -73,17 +72,16 @@ stubPreparedMessages.set("GET request up
     "transferredSize": 904
   },
   "source": "network",
   "type": "log",
   "groupId": null,
   "totalTime": 16,
   "indent": 0,
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
     "unicodeUrl": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
@@ -104,17 +102,16 @@ stubPreparedMessages.set("XHR GET reques
   "response": {},
   "source": "network",
   "type": "log",
   "groupId": null,
   "timeStamp": 1487022057746,
   "indent": 0,
   "updates": [],
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "private": false,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
@@ -152,17 +149,16 @@ stubPreparedMessages.set("XHR GET reques
     "transferredSize": 904
   },
   "source": "network",
   "type": "log",
   "groupId": null,
   "totalTime": 16,
   "indent": 0,
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
     "unicodeUrl": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
@@ -183,17 +179,16 @@ stubPreparedMessages.set("XHR POST reque
   "response": {},
   "source": "network",
   "type": "log",
   "groupId": null,
   "timeStamp": 1487022058414,
   "indent": 0,
   "updates": [],
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "private": false,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
@@ -231,17 +226,16 @@ stubPreparedMessages.set("XHR POST reque
     "transferredSize": 904
   },
   "source": "network",
   "type": "log",
   "groupId": null,
   "totalTime": 10,
   "indent": 0,
   "openedOnce": false,
-  "securityState": null,
   "securityInfo": null,
   "requestHeadersFromUploadStream": null,
   "url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
   "urlDetails": {
     "baseNameWithQuery": "inexistent.html",
     "host": "example.com",
     "scheme": "http",
     "unicodeUrl": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_return_key.js
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_return_key.js
@@ -1,29 +1,30 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// See Bug 585991.
+// Test that the Enter keys works as expected. See Bug 585991 and 1483880.
 
 const TEST_URI = `data:text/html;charset=utf-8,
 <head>
   <script>
     /* Create a prototype-less object so popup does not contain native
      * Object prototype properties.
      */
     window.foobar = Object.create(null);
     Object.assign(window.foobar, {
       item0: "value0",
       item1: "value1",
       item2: "value2",
       item3: "value3",
+      item33: "value33",
     });
   </script>
 </head>
 <body>bug 585991 - test pressing return with open popup</body>`;
 
 add_task(async function() {
   // Run test with legacy JsTerm
   await pushPref("devtools.webconsole.jsterm.codeMirror", false);
@@ -32,46 +33,66 @@ add_task(async function() {
   await pushPref("devtools.webconsole.jsterm.codeMirror", true);
   await performTests();
 });
 
 async function performTests() {
   const { jsterm } = await openNewTabAndConsole(TEST_URI);
   const { autocompletePopup: popup } = jsterm;
 
-  const onPopUpOpen = popup.once("popup-opened");
+  let onPopUpOpen = popup.once("popup-opened");
 
   info("wait for completion suggestions: window.foobar.");
 
   jsterm.setInputValue("window.fooba");
   EventUtils.sendString("r.");
 
   await onPopUpOpen;
 
   ok(popup.isOpen, "popup is open");
 
   const expectedPopupItems = [
     "item0",
     "item1",
     "item2",
     "item3",
+    "item33",
   ];
   is(popup.itemCount, expectedPopupItems.length, "popup.itemCount is correct");
   is(popup.selectedIndex, 0, "First index from top is selected");
 
   EventUtils.synthesizeKey("KEY_ArrowUp");
 
-  is(popup.selectedIndex, 3, "index 3 is selected");
-  is(popup.selectedItem.label, "item3", "item3 is selected");
+  is(popup.selectedIndex, expectedPopupItems.length - 1, "last index is selected");
+  is(popup.selectedItem.label, "item33", "item33 is selected");
   const prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
-  checkJsTermCompletionValue(jsterm, prefix + "item3", "completeNode.value holds item3");
+  checkJsTermCompletionValue(jsterm, prefix + "item33",
+    "completeNode.value holds item33");
 
   info("press Return to accept suggestion. wait for popup to hide");
-  const onPopupClose = popup.once("popup-closed");
+  let onPopupClose = popup.once("popup-closed");
   EventUtils.synthesizeKey("KEY_Enter");
 
   await onPopupClose;
 
   ok(!popup.isOpen, "popup is not open after KEY_Enter");
+  is(jsterm.getInputValue(), "window.foobar.item33",
+    "completion was successful after KEY_Enter");
+  ok(!getJsTermCompletionValue(jsterm), "completeNode is empty");
+
+  info("Test that hitting enter when the completeNode is empty closes the popup");
+  onPopUpOpen = popup.once("popup-opened");
+  info("wait for completion suggestions: window.foobar.item3");
+  jsterm.setInputValue("window.foobar.item");
+  EventUtils.sendString("3");
+  await onPopUpOpen;
+
+  is(popup.selectedItem.label, "item3", "item3 is selected");
+  ok(!getJsTermCompletionValue(jsterm), "completeNode is empty");
+
+  onPopupClose = popup.once("popup-closed");
+  EventUtils.synthesizeKey("KEY_Enter");
+  await onPopupClose;
+
+  ok(!popup.isOpen, "popup is not open after KEY_Enter");
   is(jsterm.getInputValue(), "window.foobar.item3",
     "completion was successful after KEY_Enter");
-  ok(!getJsTermCompletionValue(jsterm), "completeNode is empty");
 }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_expand.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_expand.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_FILE = "test-network-request.html";
-const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+const TEST_PATH = "https://example.com/browser/devtools/client/webconsole/" +
                   "test/mochitest/";
 const TEST_URI = TEST_PATH + TEST_FILE;
 
 const NET_PREF = "devtools.webconsole.filter.net";
 const XHR_PREF = "devtools.webconsole.filter.netxhr";
 
 requestLongerTimeout(2);
 
@@ -39,16 +39,20 @@ const tabs = [{
 }, {
   id: "timings",
   testEmpty: testEmptyTimings,
   testContent: testTimings,
 }, {
   id: "stack-trace",
   testEmpty: testEmptyStackTrace,
   testContent: testStackTrace,
+}, {
+  id: "security",
+  testEmpty: testEmptySecurity,
+  testContent: testSecurity,
 }];
 
 /**
  * Main test for checking HTTP logs in the Console panel.
  */
 add_task(async function task() {
   const hud = await openNewTabAndConsole(TEST_URI);
   const currentTab = gBrowser.selectedTab;
@@ -124,23 +128,28 @@ async function openRequestBeforeUpdates(
   // Set the default panel.
   const state = hud.ui.consoleOutput.getStore().getState();
   state.ui.networkMessageActiveTabId = tab.id;
 
   // Expand network log
   const urlNode = messageNode.querySelector(".url");
   urlNode.click();
 
-  // Make sure the current tab is the expected one.
-  const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
-  is(currentTab.getAttribute("aria-selected"), "true",
-    "The correct tab is selected");
+  // Except the security tab. It isn't available till the
+  // "securityInfo" packet type is received, so doesn't
+  // fit this part of the test.
+  if (tab.id != "security") {
+    // Make sure the current tab is the expected one.
+    const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
+    is(currentTab.getAttribute("aria-selected"), "true",
+      "The correct tab is selected");
 
-  // The tab should be empty now.
-  tab.testEmpty(messageNode);
+    // The tab should be empty now.
+    tab.testEmpty(messageNode);
+  }
 
   // Wait till all updates and payload are received.
   await updates;
   await payload;
 
   // Test content of the default tab.
   await tab.testContent(messageNode);
 
@@ -153,16 +162,17 @@ async function openRequestBeforeUpdates(
 async function testNetworkMessage(toolbox, messageNode) {
   await testStatusInfo(messageNode);
   await testHeaders(messageNode);
   await testCookies(messageNode);
   await testParams(messageNode);
   await testResponse(messageNode);
   await testTimings(messageNode);
   await testStackTrace(messageNode);
+  await testSecurity(messageNode);
   await waitForLazyRequests(toolbox);
 }
 
 // Status Info
 
 function testStatusInfo(messageNode) {
   const statusInfo = messageNode.querySelector(".status-info");
   ok(statusInfo, "Status info is not empty");
@@ -284,16 +294,34 @@ async function testStackTrace(messageNod
 
   // Select Timings tab and check the content.
   stackTraceTab.click();
   await waitUntil(() => {
     return !!messageNode.querySelector("#stack-trace-panel .frame-link");
   });
 }
 
+// Security
+
+function testEmptySecurity(messageNode) {
+  const panel = messageNode.querySelector("#security-panel .tab-panel");
+  is(panel.textContent, "", "Security tab is empty");
+}
+
+async function testSecurity(messageNode) {
+  const securityTab = messageNode.querySelector("#security-tab");
+  ok(securityTab, "Security tab is available");
+
+  // Select Timings tab and check the content.
+  securityTab.click();
+  await waitUntil(() => {
+    return !!messageNode.querySelector("#security-panel .treeTable .treeRow");
+  });
+}
+
 // Waiting helpers
 
 async function waitForPayloadReady(toolbox) {
   const {ui} = toolbox.getCurrentPanel().hud;
   return new Promise(resolve => {
     ui.jsterm.hud.on("network-request-payload-ready", () => {
       info("network-request-payload-ready received");
       resolve();
@@ -313,17 +341,17 @@ async function waitForRequestUpdates(too
     ui.jsterm.hud.on("network-message-updated", () => {
       info("network-message-updated received");
       resolve();
     });
   });
 }
 
 /**
- * Wait until all lazily fetch requests in netmonitor get finsished.
+ * Wait until all lazily fetch requests in netmonitor get finished.
  * Otherwise test will be shutdown too early and cause failure.
  */
 async function waitForLazyRequests(toolbox) {
   const {ui} = toolbox.getCurrentPanel().hud;
   const proxy = ui.jsterm.hud.proxy;
   return waitUntil(() => {
     return !proxy.networkDataProvider.lazyRequestData.size;
   });
--- a/devtools/client/webconsole/utils/messages.js
+++ b/devtools/client/webconsole/utils/messages.js
@@ -265,16 +265,17 @@ function transformNetworkEventPacket(pac
     timeStamp: networkEvent.timeStamp,
     totalTime: networkEvent.totalTime,
     url: networkEvent.request.url,
     urlDetails: getUrlDetails(networkEvent.request.url),
     method: networkEvent.request.method,
     updates: networkEvent.updates,
     cause: networkEvent.cause,
     private: networkEvent.private,
+    securityState: networkEvent.securityState,
   });
 }
 
 function transformEvaluationResultPacket(packet) {
   let {
     exceptionMessage,
     exceptionDocURL,
     exception,
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1490,16 +1490,21 @@ WebConsoleActor.prototype =
    */
   getRequestContentForURL(url) {
     if (!this.netmonitors) {
       return null;
     }
     return new Promise(resolve => {
       let messagesReceived = 0;
       const onMessage = ({ data }) => {
+        // Resolve early if the console actor is destroyed
+        if (!this.netmonitors) {
+          resolve(null);
+          return;
+        }
         if (data.url != url) {
           return;
         }
         messagesReceived++;
         // Either use the first response with a content, or return a null content
         // if we received the responses from all the message managers.
         if (data.content || messagesReceived == this.netmonitors.length) {
           for (const { messageManager } of this.netmonitors) {
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -160,17 +160,17 @@ WebConsoleClient.prototype = {
         networkInfo.response.bodySize = packet.contentSize;
         networkInfo.response.transferredSize = packet.transferredSize;
         networkInfo.discardResponseBody = packet.discardResponseBody;
         break;
       case "eventTimings":
         networkInfo.totalTime = packet.totalTime;
         break;
       case "securityInfo":
-        networkInfo.securityInfo = packet.state;
+        networkInfo.securityState = packet.state;
         break;
       case "responseCache":
         networkInfo.response.responseCache = packet.responseCache;
         break;
     }
 
     this.emit("networkEventUpdate", {
       packet: packet,
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -2889,20 +2889,23 @@ nsGenericHTMLElement::NewURIFromString(c
     // and waiting for the subsequent load to fail.
     NS_RELEASE(*aURI);
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
   return NS_OK;
 }
 
-static bool
-IsOrHasAncestorWithDisplayNone(Element* aElement)
+// https://html.spec.whatwg.org/#being-rendered
+//
+// With a gotcha for display contents:
+//   https://github.com/whatwg/html/issues/3947
+static bool IsRendered(const Element& aElement)
 {
-  return !aElement->HasServoData() || Servo_Element_IsDisplayNone(aElement);
+  return aElement.GetPrimaryFrame() || aElement.IsDisplayContents();
 }
 
 void
 nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
                                    mozilla::ErrorResult& aError)
 {
   // innerText depends on layout. For example, white space processing is
   // something that happens during reflow and which must be reflected by
@@ -2953,17 +2956,17 @@ nsGenericHTMLElement::GetInnerText(mozil
     frame = frame->GetInFlowParent();
   }
 
   // Flush layout if we determined a reflow is required.
   if (dirty && doc) {
     doc->FlushPendingNotifications(FlushType::Layout);
   }
 
-  if (IsOrHasAncestorWithDisplayNone(this)) {
+  if (!IsRendered(*this)) {
     GetTextContentInternal(aValue, aError);
   } else {
     nsRange::GetInnerTextNoFlush(aValue, aError, this);
   }
 }
 
 void
 nsGenericHTMLElement::SetInnerText(const nsAString& aValue)
--- a/dom/media/MediaStreamGraph.cpp
+++ b/dom/media/MediaStreamGraph.cpp
@@ -946,58 +946,80 @@ MediaStreamGraphImpl::CloseAudioInput(Ma
   this->AppendMessage(MakeUnique<Message>(this, aID, aListener));
 }
 
 // All AudioInput listeners get the same speaker data (at least for now).
 void
 MediaStreamGraphImpl::NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
                                        TrackRate aRate, uint32_t aChannels)
 {
+#ifdef ANDROID
+  // On Android, mInputDeviceID is always null and represents the default
+  // device.
+  // The absence of an input consumer is enough to know we need to bail out
+  // here.
+  if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
+    return;
+  }
+#else
   if (!mInputDeviceID) {
     return;
   }
+#endif
   // When/if we decide to support multiple input devices per graph, this needs
   // to loop over them.
   nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
   MOZ_ASSERT(listeners);
   for (auto& listener : *listeners) {
     listener->NotifyOutputData(this, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
 void
 MediaStreamGraphImpl::NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
                                       TrackRate aRate, uint32_t aChannels)
 {
+#ifdef ANDROID
+  if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
+    return;
+  }
+#else
 #ifdef DEBUG
   {
     MonitorAutoLock lock(mMonitor);
     // Either we have an audio input device, or we just removed the audio input
     // this iteration, and we're switching back to an output-only driver next
     // iteration.
     MOZ_ASSERT(mInputDeviceID || CurrentDriver()->Switching());
   }
 #endif
   if (!mInputDeviceID) {
     return;
   }
+#endif
   nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
   MOZ_ASSERT(listeners);
   for (auto& listener : *listeners) {
     listener->NotifyInputData(this, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
 void MediaStreamGraphImpl::DeviceChangedImpl()
 {
   MOZ_ASSERT(OnGraphThread());
 
+#ifdef ANDROID
+  if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
+    return;
+  }
+#else
   if (!mInputDeviceID) {
     return;
   }
+#endif
 
   nsTArray<RefPtr<AudioDataListener>>* listeners =
     mInputDeviceUsers.GetValue(mInputDeviceID);
   for (auto& listener : *listeners) {
     listener->DeviceChanged(this);
   }
 }
 
--- a/dom/media/MediaStreamGraphImpl.h
+++ b/dom/media/MediaStreamGraphImpl.h
@@ -463,24 +463,28 @@ public:
    * The audio input channel count for a MediaStreamGraph is the max of all the
    * channel counts requested by the listeners. The max channel count is
    * delivered to the listeners themselves, and they take care of downmixing.
    */
   uint32_t AudioInputChannelCount()
   {
     MOZ_ASSERT(OnGraphThreadOrNotRunning());
 
+#ifdef ANDROID
+    if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
+      return 0;
+    }
+#else
     if (!mInputDeviceID) {
-#ifndef ANDROID
       MOZ_ASSERT(mInputDeviceUsers.Count() == 0,
         "If running on a platform other than android,"
         "an explicit device id should be present");
-#endif
       return 0;
     }
+#endif
     uint32_t maxInputChannels = 0;
     // When/if we decide to support multiple input device per graph, this needs
     // loop over them.
     nsTArray<RefPtr<AudioDataListener>>* listeners =
       mInputDeviceUsers.GetValue(mInputDeviceID);
     MOZ_ASSERT(listeners);
     for (const auto& listener : *listeners) {
       maxInputChannels =
--- a/dom/quota/MemoryOutputStream.cpp
+++ b/dom/quota/MemoryOutputStream.cpp
@@ -19,19 +19,17 @@ MemoryOutputStream::Create(uint64_t aSiz
   MOZ_ASSERT(aSize, "Passed zero size!");
 
   if (NS_WARN_IF(aSize > UINT32_MAX)) {
     return nullptr;
   }
 
   RefPtr<MemoryOutputStream> stream = new MemoryOutputStream();
 
-  char* dummy;
-  uint32_t length = stream->mData.GetMutableData(&dummy, aSize, fallible);
-  if (NS_WARN_IF(length != aSize)) {
+  if (NS_WARN_IF(!stream->mData.SetLength(aSize, fallible))) {
     return nullptr;
   }
 
   return stream.forget();
 }
 
 NS_IMPL_ISUPPORTS(MemoryOutputStream, nsIOutputStream)
 
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -1002,16 +1002,75 @@ protected: // Shouldn't be used by frien
    *                            there is no next <tr> element, this returns
    *                            nullptr but does not return error.
    */
   Element*
   GetNextTableRowElement(Element& aTableRowElement,
                          ErrorResult& aRv) const;
 
   /**
+   * CellIndexes store both row index and column index of a table cell.
+   */
+  struct MOZ_STACK_CLASS CellIndexes final
+  {
+    int32_t mRow;
+    int32_t mColumn;
+
+    /**
+     * This constructor initializes mRowIndex and mColumnIndex with indexes of
+     * aCellElement.
+     *
+     * @param aCellElement      An <td> or <th> element.
+     * @param aRv               Returns error if layout information is not
+     *                          available or given element is not a table cell.
+     */
+    CellIndexes(Element& aCellElement, ErrorResult& aRv)
+      : mRow(-1)
+      , mColumn(-1)
+    {
+      MOZ_ASSERT(!aRv.Failed());
+      Update(aCellElement, aRv);
+    }
+
+    /**
+     * Update mRowIndex and mColumnIndex with indexes of aCellElement.
+     *
+     * @param                   See above.
+     */
+    void Update(Element& aCellElement, ErrorResult& aRv);
+
+    /**
+     * This constructor initializes mRowIndex and mColumnIndex with indexes of
+     * cell element which contains anchor of Selection.
+     *
+     * @param aHTMLEditor       The editor which creates the instance.
+     * @param aSelection        The Selection for the editor.
+     * @param aRv               Returns error if there is no cell element
+     *                          which contains anchor of Selection, or layout
+     *                          information is not available.
+     */
+    CellIndexes(HTMLEditor& aHTMLEditor, Selection& aSelection,
+                ErrorResult& aRv)
+      : mRow(-1)
+      , mColumn(-1)
+    {
+      Update(aHTMLEditor, aSelection, aRv);
+    }
+
+    /**
+     * Update mRowIndex and mColumnIndex with indexes of cell element which
+     * contains anchor of Selection.
+     *
+     * @param                   See above.
+     */
+    void Update(HTMLEditor& aHTMLEditor, Selection& aSelection,
+                ErrorResult& aRv);
+  };
+
+  /**
    * PasteInternal() pasts text with replacing selected content.
    * This tries to dispatch ePaste event first.  If its defaultPrevent() is
    * called, this does nothing but returns NS_OK.
    *
    * @param aClipboardType  nsIClipboard::kGlobalClipboard or
    *                        nsIClipboard::kSelectionClipboard.
    */
   nsresult PasteInternal(int32_t aClipboardType);
--- a/editor/libeditor/HTMLTableEditor.cpp
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -757,28 +757,30 @@ HTMLEditor::DeleteTableCell(int32_t aNum
   AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
                                       *this, EditSubAction::eDeleteNode,
                                       nsIEditor::eNext);
 
   RefPtr<Element> firstCell;
   rv = GetFirstSelectedCell(nullptr, getter_AddRefs(firstCell));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // When 2 or more cells are selected, ignore aNumber and use selected cells.
   if (firstCell && selection->RangeCount() > 1) {
-    // When > 1 selected cell,
-    //  ignore aNumber and use selected cells
-    cell = firstCell;
-
     int32_t rowCount, colCount;
     rv = GetTableSize(table, &rowCount, &colCount);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    // Get indexes -- may be different than original cell
-    rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-    NS_ENSURE_SUCCESS(rv, rv);
+    ErrorResult error;
+    CellIndexes firstCellIndexes(*firstCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
+    cell = firstCell;
+    startRowIndex = firstCellIndexes.mRow;
+    startColIndex = firstCellIndexes.mColumn;
 
     // The setCaret object will call AutoSelectionSetterAfterTableEdit in its
     // destructor
     AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
                                                startColIndex, ePreviousColumn,
                                                false);
     AutoTransactionsConserveSelection dontChangeSelection(*this);
 
@@ -799,18 +801,22 @@ HTMLEditor::DeleteTableCell(int32_t aNum
           //   to continue after we delete this row
           int32_t nextRow = startRowIndex;
           while (nextRow == startRowIndex) {
             rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
             NS_ENSURE_SUCCESS(rv, rv);
             if (!cell) {
               break;
             }
-            rv = GetCellIndexes(cell, &nextRow, &startColIndex);
-            NS_ENSURE_SUCCESS(rv, rv);
+            CellIndexes nextSelectedCellIndexes(*cell, error);
+            if (NS_WARN_IF(error.Failed())) {
+              return error.StealNSResult();
+            }
+            nextRow = nextSelectedCellIndexes.mRow;
+            startColIndex = nextSelectedCellIndexes.mColumn;
           }
           // Delete entire row
           rv = DeleteRow(table, startRowIndex);
           NS_ENSURE_SUCCESS(rv, rv);
 
           if (cell) {
             // For the next cell: Subtract 1 for row we deleted
             startRowIndex = nextRow - 1;
@@ -831,18 +837,22 @@ HTMLEditor::DeleteTableCell(int32_t aNum
             //   to continue after we delete this column
             int32_t nextCol = startColIndex;
             while (nextCol == startColIndex) {
               rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
               NS_ENSURE_SUCCESS(rv, rv);
               if (!cell) {
                 break;
               }
-              rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
-              NS_ENSURE_SUCCESS(rv, rv);
+              CellIndexes nextSelectedCellIndexes(*cell, error);
+              if (NS_WARN_IF(error.Failed())) {
+                return error.StealNSResult();
+              }
+              startRowIndex = nextSelectedCellIndexes.mRow;
+              nextCol = nextSelectedCellIndexes.mColumn;
             }
             // Delete entire Col
             rv = DeleteColumn(table, startColIndex);
             NS_ENSURE_SUCCESS(rv, rv);
             if (cell) {
               // For the next cell, subtract 1 for col. deleted
               startColIndex = nextCol - 1;
               // Set true since we know we will look at a new column next
@@ -858,21 +868,25 @@ HTMLEditor::DeleteTableCell(int32_t aNum
 
           // Then delete the cell
           rv = DeleteNodeWithTransaction(*cell);
           if (NS_WARN_IF(NS_FAILED(rv))) {
             return rv;
           }
 
           // The next cell to delete
+          if (nextCell) {
+            CellIndexes nextCellIndexes(*nextCell, error);
+            if (NS_WARN_IF(error.Failed())) {
+              return error.StealNSResult();
+            }
+            startRowIndex = nextCellIndexes.mRow;
+            startColIndex = nextCellIndexes.mColumn;
+          }
           cell = nextCell;
-          if (cell) {
-            rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-            NS_ENSURE_SUCCESS(rv, rv);
-          }
         }
       }
     }
   } else {
     for (int32_t i = 0; i < aNumber; i++) {
       rv = GetCellContext(getter_AddRefs(selection),
                           getter_AddRefs(table),
                           getter_AddRefs(cell),
@@ -952,19 +966,24 @@ HTMLEditor::DeleteTableCellContents()
 
 
   RefPtr<Element> firstCell;
   rv = GetFirstSelectedCell(nullptr, getter_AddRefs(firstCell));
   NS_ENSURE_SUCCESS(rv, rv);
 
 
   if (firstCell) {
+    ErrorResult error;
+    CellIndexes firstCellIndexes(*firstCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
     cell = firstCell;
-    rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-    NS_ENSURE_SUCCESS(rv, rv);
+    startRowIndex = firstCellIndexes.mRow;
+    startColIndex = firstCellIndexes.mColumn;
   }
 
   AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
                                              startColIndex, ePreviousColumn,
                                              false);
 
   while (cell) {
     DeleteCellContents(cell);
@@ -1032,46 +1051,58 @@ HTMLEditor::DeleteTableColumn(int32_t aN
 
   // Test if deletion is controlled by selected cells
   RefPtr<Element> firstCell;
   rv = GetFirstSelectedCell(nullptr, getter_AddRefs(firstCell));
   NS_ENSURE_SUCCESS(rv, rv);
 
   uint32_t rangeCount = selection->RangeCount();
 
+  ErrorResult error;
   if (firstCell && rangeCount > 1) {
-    // Fetch indexes again - may be different for selected cells
-    rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
-    NS_ENSURE_SUCCESS(rv, rv);
+    CellIndexes firstCellIndexes(*firstCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
+    startRowIndex = firstCellIndexes.mRow;
+    startColIndex = firstCellIndexes.mColumn;
   }
   //We control selection resetting after the insert...
   AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
                                              startColIndex, ePreviousRow,
                                              false);
 
   if (firstCell && rangeCount > 1) {
     // Use selected cells to determine what rows to delete
     cell = firstCell;
 
     while (cell) {
       if (cell != firstCell) {
-        rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-        NS_ENSURE_SUCCESS(rv, rv);
+        CellIndexes cellIndexes(*cell, error);
+        if (NS_WARN_IF(error.Failed())) {
+          return error.StealNSResult();
+        }
+        startRowIndex = cellIndexes.mRow;
+        startColIndex = cellIndexes.mColumn;
       }
       // Find the next cell in a different column
       // to continue after we delete this column
       int32_t nextCol = startColIndex;
       while (nextCol == startColIndex) {
         rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
         NS_ENSURE_SUCCESS(rv, rv);
         if (!cell) {
           break;
         }
-        rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
-        NS_ENSURE_SUCCESS(rv, rv);
+        CellIndexes cellIndexes(*cell, error);
+        if (NS_WARN_IF(error.Failed())) {
+          return error.StealNSResult();
+        }
+        startRowIndex = cellIndexes.mRow;
+        nextCol = cellIndexes.mColumn;
       }
       rv = DeleteColumn(table, startColIndex);
       NS_ENSURE_SUCCESS(rv, rv);
     }
   } else {
     for (int32_t i = 0; i < aNumber; i++) {
       rv = DeleteColumn(table, startColIndex);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -1196,47 +1227,62 @@ HTMLEditor::DeleteTableRow(int32_t aNumb
                                       nsIEditor::eNext);
 
   RefPtr<Element> firstCell;
   rv = GetFirstSelectedCell(nullptr, getter_AddRefs(firstCell));
   NS_ENSURE_SUCCESS(rv, rv);
 
   uint32_t rangeCount = selection->RangeCount();
 
+  ErrorResult error;
   if (firstCell && rangeCount > 1) {
     // Fetch indexes again - may be different for selected cells
-    rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
-    NS_ENSURE_SUCCESS(rv, rv);
+    CellIndexes firstCellIndexes(*firstCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
+    startRowIndex = firstCellIndexes.mRow;
+    startColIndex = firstCellIndexes.mColumn;
   }
 
   //We control selection resetting after the insert...
   AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
                                              startColIndex, ePreviousRow,
                                              false);
   // Don't change selection during deletions
   AutoTransactionsConserveSelection dontChangeSelection(*this);
 
   if (firstCell && rangeCount > 1) {
     // Use selected cells to determine what rows to delete
     cell = firstCell;
 
     while (cell) {
       if (cell != firstCell) {
-        rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-        NS_ENSURE_SUCCESS(rv, rv);
+        CellIndexes cellIndexes(*cell, error);
+        if (NS_WARN_IF(error.Failed())) {
+          return error.StealNSResult();
+        }
+        startRowIndex = cellIndexes.mRow;
+        startColIndex = cellIndexes.mColumn;
       }
       // Find the next cell in a different row
       // to continue after we delete this row
       int32_t nextRow = startRowIndex;
       while (nextRow == startRowIndex) {
         rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
         NS_ENSURE_SUCCESS(rv, rv);
-        if (!cell) break;
-        rv = GetCellIndexes(cell, &nextRow, &startColIndex);
-        NS_ENSURE_SUCCESS(rv, rv);
+        if (!cell) {
+          break;
+        }
+        CellIndexes cellIndexes(*cell, error);
+        if (NS_WARN_IF(error.Failed())) {
+          return error.StealNSResult();
+        }
+        nextRow = cellIndexes.mRow;
+        startColIndex = cellIndexes.mColumn;
       }
       // Delete entire row
       rv = DeleteRow(table, startRowIndex);
       NS_ENSURE_SUCCESS(rv, rv);
     }
   } else {
     // Check for counts too high
     aNumber = std::min(aNumber,(rowCount-startRowIndex));
@@ -1429,55 +1475,60 @@ HTMLEditor::SelectBlockOfCells(Element* 
   }
 
   // We can only select a block if within the same table,
   //  so do nothing if not within one table
   if (table != endTable) {
     return NS_OK;
   }
 
-  int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
-
-  // Get starting and ending cells' location in the cellmap
-  nsresult rv = GetCellIndexes(aStartCell, &startRowIndex, &startColIndex);
-  if (NS_FAILED(rv)) {
-    return rv;
+  ErrorResult error;
+  CellIndexes startCellIndexes(*aStartCell, error);
+  if (NS_WARN_IF(error.Failed())) {
+    return error.StealNSResult();
   }
-
-  rv = GetCellIndexes(aEndCell, &endRowIndex, &endColIndex);
-  if (NS_FAILED(rv)) {
-    return rv;
+  CellIndexes endCellIndexes(*aEndCell, error);
+  if (NS_WARN_IF(error.Failed())) {
+    return error.StealNSResult();
   }
 
   // Suppress nsISelectionListener notification
   //  until all selection changes are finished
   SelectionBatcher selectionBatcher(selection);
 
   // Examine all cell nodes in current selection and
   //  remove those outside the new block cell region
-  int32_t minColumn = std::min(startColIndex, endColIndex);
-  int32_t minRow    = std::min(startRowIndex, endRowIndex);
-  int32_t maxColumn   = std::max(startColIndex, endColIndex);
-  int32_t maxRow      = std::max(startRowIndex, endRowIndex);
+  int32_t minColumn =
+    std::min(startCellIndexes.mColumn, endCellIndexes.mColumn);
+  int32_t minRow =
+    std::min(startCellIndexes.mRow, endCellIndexes.mRow);
+  int32_t maxColumn =
+    std::max(startCellIndexes.mColumn, endCellIndexes.mColumn);
+  int32_t maxRow =
+    std::max(startCellIndexes.mRow, endCellIndexes.mRow);
 
   RefPtr<Element> cell;
   int32_t currentRowIndex, currentColIndex;
   RefPtr<nsRange> range;
-  rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
+  nsresult rv =
+    GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
   NS_ENSURE_SUCCESS(rv, rv);
   if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) {
     return NS_OK;
   }
 
   while (cell) {
-    rv = GetCellIndexes(cell, &currentRowIndex, &currentColIndex);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    if (currentRowIndex < maxRow || currentRowIndex > maxRow ||
-        currentColIndex < maxColumn || currentColIndex > maxColumn) {
+    CellIndexes currentCellIndexes(*cell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
+    }
+    if (currentCellIndexes.mRow < maxRow ||
+        currentCellIndexes.mRow > maxRow ||
+        currentCellIndexes.mColumn < maxColumn ||
+        currentCellIndexes.mColumn > maxColumn) {
       selection->RemoveRange(*range, IgnoreErrors());
       // Since we've removed the range, decrement pointer to next range
       mSelectedCellIndex--;
     }
     rv = GetNextSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
@@ -2620,53 +2671,95 @@ HTMLEditor::NormalizeTable(Element* aTab
         previousCellInRow = cell;
       }
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
-HTMLEditor::GetCellIndexes(Element* aCell,
+HTMLEditor::GetCellIndexes(Element* aCellElement,
                            int32_t* aRowIndex,
-                           int32_t* aColIndex)
+                           int32_t* aColumnIndex)
 {
-  NS_ENSURE_ARG_POINTER(aRowIndex);
-  *aColIndex=0; // initialize out params
-  NS_ENSURE_ARG_POINTER(aColIndex);
-  *aRowIndex=0;
-  // Needs to stay alive while we're using aCell, since it may be keeping it
-  // alive.
-  // XXX Looks like it's safe to use raw pointer here.  However, layout code
-  //     change won't be handled by editor developers so that it must be safe
-  //     to keep using RefPtr here.
-  RefPtr<Element> cell;
-  if (!aCell) {
+  if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  *aRowIndex = 0;
+  *aColumnIndex = 0;
+
+  if (!aCellElement) {
+    // Use cell element which contains anchor of Selection when aCellElement is
+    // nullptr.
     RefPtr<Selection> selection = GetSelection();
     if (NS_WARN_IF(!selection)) {
       return NS_ERROR_FAILURE;
     }
-    // Get the selected cell or the cell enclosing the selection anchor
-    cell = GetElementOrParentByTagNameAtSelection(*selection, *nsGkAtoms::td);
-    if (!cell) {
-      return NS_ERROR_FAILURE;
+    ErrorResult error;
+    CellIndexes cellIndexes(*this, *selection, error);
+    if (error.Failed()) {
+      return error.StealNSResult();
     }
-    aCell = cell;
+    *aRowIndex = cellIndexes.mRow;
+    *aColumnIndex = cellIndexes.mColumn;
+    return NS_OK;
+  }
+
+  ErrorResult error;
+  CellIndexes cellIndexes(*aCellElement, error);
+  if (NS_WARN_IF(error.Failed())) {
+    return error.StealNSResult();
   }
-
-  nsCOMPtr<nsIPresShell> ps = GetPresShell();
-  NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
-
-  // frames are not ref counted, so don't use an nsCOMPtr
-  nsIFrame *layoutObject = aCell->GetPrimaryFrame();
-  NS_ENSURE_TRUE(layoutObject, NS_ERROR_FAILURE);
-
-  nsITableCellLayout *cellLayoutObject = do_QueryFrame(layoutObject);
-  NS_ENSURE_TRUE(cellLayoutObject, NS_ERROR_FAILURE);
-  return cellLayoutObject->GetCellIndexes(*aRowIndex, *aColIndex);
+  *aRowIndex = cellIndexes.mRow;
+  *aColumnIndex = cellIndexes.mColumn;
+  return NS_OK;
+}
+
+void
+HTMLEditor::CellIndexes::Update(HTMLEditor& aHTMLEditor,
+                                Selection& aSelection,
+                                ErrorResult& aRv)
+{
+  MOZ_ASSERT(!aRv.Failed());
+
+  // Guarantee the life time of the cell element since Init() will access
+  // layout methods.
+  RefPtr<Element> cellElement =
+    aHTMLEditor.GetElementOrParentByTagNameAtSelection(aSelection,
+                                                       *nsGkAtoms::td);
+  if (!cellElement) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  Update(*cellElement, aRv);
+}
+
+void
+HTMLEditor::CellIndexes::Update(Element& aCellElement,
+                                ErrorResult& aRv)
+{
+  MOZ_ASSERT(!aRv.Failed());
+
+  // XXX If the table cell is created immediately before this call, e.g.,
+  //     using innerHTML, frames have not been created yet.  In such case,
+  //     shouldn't we flush pending layout?
+  nsIFrame* frameOfCell = aCellElement.GetPrimaryFrame();
+  if (NS_WARN_IF(!frameOfCell)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  nsITableCellLayout* tableCellLayout = do_QueryFrame(frameOfCell);
+  if (!tableCellLayout) {
+    aRv.Throw(NS_ERROR_FAILURE); // not a cell element.
+    return;
+  }
+
+  aRv = tableCellLayout->GetCellIndexes(mRow, mColumn);
+  NS_WARNING_ASSERTION(!aRv.Failed(), "Failed to get cell indexes");
 }
 
 nsTableWrapperFrame*
 HTMLEditor::GetTableFrame(Element* aTable)
 {
   NS_ENSURE_TRUE(aTable, nullptr);
   return do_QueryFrame(aTable->GetPrimaryFrame());
 }
@@ -2879,17 +2972,17 @@ HTMLEditor::GetCellSpansAt(Element* aTab
 
 nsresult
 HTMLEditor::GetCellContext(Selection** aSelection,
                            Element** aTable,
                            Element** aCell,
                            nsINode** aCellParent,
                            int32_t* aCellOffset,
                            int32_t* aRowIndex,
-                           int32_t* aColIndex)
+                           int32_t* aColumnIndex)
 {
   // Initialize return pointers
   if (aSelection) {
     *aSelection = nullptr;
   }
   if (aTable) {
     *aTable = nullptr;
   }
@@ -2900,18 +2993,18 @@ HTMLEditor::GetCellContext(Selection** a
     *aCellParent = nullptr;
   }
   if (aCellOffset) {
     *aCellOffset = 0;
   }
   if (aRowIndex) {
     *aRowIndex = 0;
   }
-  if (aColIndex) {
-    *aColIndex = 0;
+  if (aColumnIndex) {
+    *aColumnIndex = 0;
   }
 
   RefPtr<Selection> selection = GetSelection();
   if (NS_WARN_IF(!selection)) {
     return NS_ERROR_FAILURE;
   }
 
   if (aSelection) {
@@ -2964,28 +3057,27 @@ HTMLEditor::GetCellContext(Selection** a
     // Cell must be in a table, so fail if not found
     return NS_ERROR_FAILURE;
   }
   if (aTable) {
     table.forget(aTable);
   }
 
   // Get the rest of the related data only if requested
-  if (aRowIndex || aColIndex) {
-    int32_t rowIndex, colIndex;
-    // Get current cell location so we can put caret back there when done
-    nsresult rv = GetCellIndexes(cell, &rowIndex, &colIndex);
-    if (NS_FAILED(rv)) {
-      return rv;
+  if (aRowIndex || aColumnIndex) {
+    ErrorResult error;
+    CellIndexes cellIndexes(*cell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
     }
     if (aRowIndex) {
-      *aRowIndex = rowIndex;
+      *aRowIndex = cellIndexes.mRow;
     }
-    if (aColIndex) {
-      *aColIndex = colIndex;
+    if (aColumnIndex) {
+      *aColumnIndex = cellIndexes.mColumn;
     }
   }
   if (aCellParent) {
     // Get the immediate parent of the cell
     nsCOMPtr<nsINode> cellParent = cell->GetParentNode();
     // Cell has to have a parent, so fail if not found
     NS_ENSURE_TRUE(cellParent, NS_ERROR_FAILURE);
 
@@ -3130,52 +3222,52 @@ HTMLEditor::GetNextSelectedCell(nsRange*
   // Setup for next cell
   mSelectedCellIndex++;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
-                                        int32_t* aColIndex,
+                                        int32_t* aColumnIndex,
                                         Element** aCell)
 {
   NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
   *aCell = nullptr;
   if (aRowIndex) {
     *aRowIndex = 0;
   }
-  if (aColIndex) {
-    *aColIndex = 0;
+  if (aColumnIndex) {
+    *aColumnIndex = 0;
   }
 
   RefPtr<Element> cell;
   nsresult rv = GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(cell, NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND);
 
   // We don't want to cell.forget() here, because we use "cell" below.
   *aCell = do_AddRef(cell).take();
 
+  if (!aRowIndex && !aColumnIndex) {
+    return NS_OK;
+  }
+
   // Also return the row and/or column if requested
-  if (aRowIndex || aColIndex) {
-    int32_t startRowIndex, startColIndex;
-    rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-
-    if (aRowIndex) {
-      *aRowIndex = startRowIndex;
-    }
-    if (aColIndex) {
-      *aColIndex = startColIndex;
-    }
+  ErrorResult error;
+  CellIndexes cellIndexes(*cell, error);
+  if (NS_WARN_IF(error.Failed())) {
+    return error.StealNSResult();
   }
-
+  if (aRowIndex) {
+    *aRowIndex = cellIndexes.mRow;
+  }
+  if (aColumnIndex) {
+    *aColumnIndex = cellIndexes.mColumn;
+  }
   return NS_OK;
 }
 
 void
 HTMLEditor::SetSelectionAfterTableEdit(Element* aTable,
                                        int32_t aRow,
                                        int32_t aCol,
                                        int32_t aDirection,
@@ -3366,29 +3458,28 @@ HTMLEditor::GetSelectedCellsType(Element
   }
 
   // We have at least one selected cell, so set return value
   *aSelectionType = static_cast<uint32_t>(TableSelection::Cell);
 
   // Store indexes of each row/col to avoid duplication of searches
   nsTArray<int32_t> indexArray;
 
+  ErrorResult error;
   bool allCellsInRowAreSelected = false;
   bool allCellsInColAreSelected = false;
   while (NS_SUCCEEDED(rv) && selectedCell) {
-    // Get the cell's location in the cellmap
-    int32_t startRowIndex, startColIndex;
-    rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
-    if (NS_FAILED(rv)) {
-      return rv;
+    CellIndexes selectedCellIndexes(*selectedCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
     }
-
-    if (!indexArray.Contains(startColIndex)) {
-      indexArray.AppendElement(startColIndex);
-      allCellsInRowAreSelected = AllCellsInRowSelected(table, startRowIndex, colCount);
+    if (!indexArray.Contains(selectedCellIndexes.mColumn)) {
+      indexArray.AppendElement(selectedCellIndexes.mColumn);
+      allCellsInRowAreSelected =
+        AllCellsInRowSelected(table, selectedCellIndexes.mRow, colCount);
       // We're done as soon as we fail for any row
       if (!allCellsInRowAreSelected) {
         break;
       }
     }
     rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell));
   }
 
@@ -3399,26 +3490,25 @@ HTMLEditor::GetSelectedCellsType(Element
   // Test for columns
 
   // Empty the indexArray
   indexArray.Clear();
 
   // Start at first cell again
   rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell));
   while (NS_SUCCEEDED(rv) && selectedCell) {
-    // Get the cell's location in the cellmap
-    int32_t startRowIndex, startColIndex;
-    rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
-    if (NS_FAILED(rv)) {
-      return rv;
+    CellIndexes selectedCellIndexes(*selectedCell, error);
+    if (NS_WARN_IF(error.Failed())) {
+      return error.StealNSResult();
     }
 
-    if (!indexArray.Contains(startRowIndex)) {
-      indexArray.AppendElement(startColIndex);
-      allCellsInColAreSelected = AllCellsInColumnSelected(table, startColIndex, rowCount);
+    if (!indexArray.Contains(selectedCellIndexes.mRow)) {
+      indexArray.AppendElement(selectedCellIndexes.mColumn);
+      allCellsInColAreSelected =
+        AllCellsInColumnSelected(table, selectedCellIndexes.mColumn, rowCount);
       // We're done as soon as we fail for any column
       if (!allCellsInRowAreSelected) {
         break;
       }
     }
     rv = GetNextSelectedCell(nullptr, getter_AddRefs(selectedCell));
   }
   if (allCellsInColAreSelected) {
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -284,16 +284,17 @@ skip-if = android_version == '24'
 subsuite = clipboard
 skip-if = android_version == '24'
 [test_nsIHTMLEditor_getSelectedElement.html]
 skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure of non-related test
 [test_nsIHTMLEditor_selectElement.html]
 skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure of non-related test
 [test_nsIHTMLEditor_setCaretAfterElement.html]
 skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure of non-related test
+[test_nsITableEditor_getCellIndexes.html]
 [test_nsITableEditor_getFirstRow.html]
 [test_resizers_appearance.html]
 [test_resizers_resizing_elements.html]
 skip-if = android_version == '18' || (verify && debug && os == 'win') # bug 1147989
 [test_root_element_replacement.html]
 [test_select_all_without_body.html]
 [test_spellcheck_pref.html]
 skip-if = toolkit == 'android'
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html
@@ -0,0 +1,92 @@
+<!DOCTYPE>
+<html>
+<head>
+  <title>Test for nsITableEditor.getCellIndexes()</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  let editor = document.getElementById("content");
+  let selection = document.getSelection();
+  let rowIndex = {}, columnIndex = {};
+
+  try {
+    getTableEditor().getCellIndexes(undefined, rowIndex, columnIndex);
+    ok(false, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
+  } catch (e) {
+    ok(true, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
+  }
+
+  try {
+    getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
+    ok(false, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
+  } catch (e) {
+    ok(true, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
+  }
+
+  try {
+    getTableEditor().getCellIndexes(editor, rowIndex, columnIndex);
+    ok(false, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
+  } catch (e) {
+    ok(true, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
+  }
+
+  // Set id to "test" for the argument for getCellIndexes().
+  // Set data-row and data-col to expected indexes.
+  kTests = [
+    '<table><tr><td id="test" data-row="0" data-col="0">cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td id="test" data-row="0" data-col="2">cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td id="test" data-row="2" data-col="0">cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td id="test" data-row="2" data-col="2">cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1" rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0" colspan="2">cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td colspan="2">cell2-1</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
+    '<table><tr><th id="test" data-row="0" data-col="0">cell1-1</th><th>cell1-2</th><th>cell1-3</tr><tr><th>cell2-1</th><th>cell2-2</th><th>cell2-3</th></tr><tr><th>cell3-1</th><th>cell3-2</th><th>cell3-3</th></tr></table>',
+  ]
+
+  for (const kTest of kTests) {
+    editor.innerHTML = kTest;
+    editor.scrollTop; // compute layout now.
+    let cell = document.getElementById("test");
+    getTableEditor().getCellIndexes(cell, rowIndex, columnIndex);
+    is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Specified cell element directly, row Index value of ${kTest}`);
+    is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Specified cell element directly, column Index value of ${kTest}`);
+    selection.collapse(cell.firstChild, 0);
+    getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
+    is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Selection is collapsed in the cell element, row Index value of ${kTest}`);
+    is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Selection is collapsed in the cell element, column Index value of ${kTest}`);
+  }
+
+  SimpleTest.finish();
+});
+
+function getTableEditor() {
+  var Ci = SpecialPowers.Ci;
+  var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+  return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
--- a/editor/nsITableEditor.idl
+++ b/editor/nsITableEditor.idl
@@ -133,22 +133,35 @@ interface nsITableEditor : nsISupports
     *   previous size of the table
     * If aTable is null, it uses table enclosing the selection anchor
     * This doesn't doesn't change the selection,
     *   thus it can be used to fixup all tables
     *   in a page independent of the selection
     */
   void normalizeTable(in Element aTable);
 
-  /** Get the row an column index from the layout's cellmap
-    * If aCell is null, it will try to find enclosing table of selection anchor
-    *
-    */
-  void getCellIndexes(in Element aCell,
-                      out long aRowIndex, out long aColIndex);
+  /**
+   * getCellIndexes() computes row index and column index of a table cell.
+   * Note that this depends on layout information.  Therefore, all pending
+   * layout should've been flushed before calling this.
+   *
+   * @param aCellElement        If not null, this computes indexes of the cell.
+   *                            If null, this computes indexes of a cell which
+   *                            contains anchor of Selection.
+   * @param aRowIndex           Must be an object, whose .value will be set
+   *                            to row index of the cell.  0 is the first row.
+   *                            If rowspan is set to 2 or more, the start
+   *                            row index is used.
+   * @param aColumnIndex        Must be an object, whose .value will be set
+   *                            to column index of the cell.  0 is the first
+   *                            column.  If colspan is set to 2 or more, the
+   *                            start column index is used.
+   */
+  void getCellIndexes(in Element aCellElement,
+                      out long aRowIndex, out long aColumnIndex);
 
   /** Get the number of rows and columns in a table from the layout's cellmap
     * If aTable is null, it will try to find enclosing table of selection ancho
     * Note that all rows in table will not have this many because of
     * ROWSPAN effects or if table is not "rectangular" (has short rows)
     */
   void getTableSize(in Element aTable,
                     out long aRowCount, out long aColCount);
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
@@ -58,16 +58,19 @@ static const int access = SandboxBroker:
 #endif
 
 static void
 AddMesaSysfsPaths(SandboxBroker::Policy* aPolicy)
 {
   // Bug 1384178: Mesa driver loader
   aPolicy->AddPrefix(rdonly, "/sys/dev/char/226:");
 
+  // Bug 1480755: Mesa tries to probe /sys paths in turn
+  aPolicy->AddAncestors("/sys/dev/char/");
+
   // Bug 1401666: Mesa driver loader part 2: Mesa <= 12 using libudev
   if (auto dir = opendir("/dev/dri")) {
     while (auto entry = readdir(dir)) {
       if (entry->d_name[0] != '.') {
         nsPrintfCString devPath("/dev/dri/%s", entry->d_name);
         struct stat sb;
         if (stat(devPath.get(), &sb) == 0 && S_ISCHR(sb.st_mode)) {
           // For both the DRI node and its parent (the physical
@@ -79,20 +82,32 @@ AddMesaSysfsPaths(SandboxBroker::Policy*
                                     minor(sb.st_rdev),
                                     suffix);
             // libudev will expand the symlink but not do full
             // canonicalization, so it will leave in ".." path
             // components that will be realpath()ed in the
             // broker.  To match this, allow the canonical paths.
             UniqueFreePtr<char[]> realSysPath(realpath(sysPath.get(), nullptr));
             if (realSysPath) {
-              nsPrintfCString ueventPath("%s/uevent", realSysPath.get());
-              nsPrintfCString configPath("%s/config", realSysPath.get());
-              aPolicy->AddPath(rdonly, ueventPath.get());
-              aPolicy->AddPath(rdonly, configPath.get());
+              static const Array<const char*, 7> kMesaAttrSuffixes = {
+                "revision",
+                "vendor",
+                "device",
+                "subsystem_vendor",
+                "subsystem_device",
+                "uevent",
+                "config"
+              };
+              for (const auto attrSuffix : kMesaAttrSuffixes) {
+                nsPrintfCString attrPath("%s/%s", realSysPath.get(), attrSuffix);
+                aPolicy->AddPath(rdonly, attrPath.get());
+              }
+              // Allowing stat-ing the parent dirs
+              nsPrintfCString basePath("%s/", realSysPath.get());
+              aPolicy->AddAncestors(basePath.get());
             }
           }
         }
       }
     }
     closedir(dir);
   }
 }
--- a/testing/mozharness/mozharness/mozilla/building/buildbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py
@@ -1493,16 +1493,17 @@ or run without that action (ie: --no-{ac
         if total > 0:
             hits /= float(total)
 
         yield {
             'name': 'sccache hit rate',
             'value': hits,
             'extraOptions': self.perfherder_resource_options(),
             'subtests': [],
+            'lowerIsBetter': False
         }
 
         yield {
             'name': 'sccache cache_write_errors',
             'value': stats['stats']['cache_write_errors'],
             'extraOptions': self.perfherder_resource_options(),
             'alertThreshold': 50.0,
             'subtests': [],
--- a/testing/web-platform/meta/html/dom/elements/the-innertext-idl-attribute/getter.html.ini
+++ b/testing/web-platform/meta/html/dom/elements/the-innertext-idl-attribute/getter.html.ini
@@ -1,9 +1,12 @@
 [getter.html]
+  [<canvas><div id='target'> contents ok for element not being rendered ("<canvas><div id='target'>abc")]
+    expected: FAIL
+
   [<rp> ("<div><ruby>abc<rp>(</rp><rt>def</rt><rp>)</rp></ruby>")]
     expected: FAIL
 
   [Lone <rp> ("<div><rp>abc</rp>")]
     expected: FAIL
 
   [display:block <rp> with whitespace ("<div><rp style='display:block'> abc </rp>def")]
     expected: FAIL
--- a/testing/web-platform/tests/html/dom/elements/the-innertext-idl-attribute/getter-tests.js
+++ b/testing/web-platform/tests/html/dom/elements/the-innertext-idl-attribute/getter-tests.js
@@ -70,17 +70,17 @@ testText("<div>&nbsp;", "\xA0", "&nbsp; 
 
 /**** display:none ****/
 
 testText("<div style='display:none'>abc", "abc", "display:none container");
 testText("<div style='display:none'>abc  def", "abc  def", "No whitespace compression in display:none container");
 testText("<div style='display:none'> abc def ", " abc def ", "No removal of leading/trailing whitespace in display:none container");
 testText("<div>123<span style='display:none'>abc", "123", "display:none child not rendered");
 testText("<div style='display:none'><span id='target'>abc", "abc", "display:none container with non-display-none target child");
-testTextInSVG("<div id='target'>abc", "", "non-display-none child of svg");
+testTextInSVG("<div id='target'>abc", "abc", "non-display-none child of svg");
 testTextInSVG("<div style='display:none' id='target'>abc", "abc", "display:none child of svg");
 testTextInSVG("<div style='display:none'><div id='target'>abc", "abc", "child of display:none child of svg");
 
 /**** display:contents ****/
 
 if (CSS.supports("display", "contents")) {
   testText("<div style='display:contents'>abc", "abc", "display:contents container");
   testText("<div><div style='display:contents'>abc", "abc", "display:contents container");
@@ -127,23 +127,23 @@ testText("<button>abc", "abc", "<button>
 testText("<fieldset>abc", "abc", "<fieldset> contents preserved");
 testText("<fieldset><legend>abc", "abc", "<fieldset> <legend> contents preserved");
 testText("<input type='text' value='abc'>", "", "<input> contents ignored");
 testText("<textarea>abc", "", "<textarea> contents ignored");
 testText("<iframe>abc", "", "<iframe> contents ignored");
 testText("<iframe><div id='target'>abc", "", "<iframe> contents ignored");
 testText("<iframe src='data:text/html,abc'>", "","<iframe> subdocument ignored");
 testText("<audio style='display:block'>abc", "", "<audio> contents ignored");
-testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "", "<audio> contents ignored");
-testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok if display:none");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "abc", "<audio> contents ok for element not being rendered");
+testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok for element not being rendered");
 testText("<video>abc", "", "<video> contents ignored");
-testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "", "<video> contents ignored");
-testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok if display:none");
+testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "abc", "<video> contents ok for element not being rendered");
+testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok for element not being rendered");
 testText("<canvas>abc", "", "<canvas> contents ignored");
-testText("<canvas><div id='target'>abc", "", "<canvas><div id='target'> contents ignored");
+testText("<canvas><div id='target'>abc", "abc", "<canvas><div id='target'> contents ok for element not being rendered");
 testText("<img alt='abc'>", "", "<img> alt text ignored");
 testText("<img src='about:blank' class='poke'>", "", "<img> contents ignored");
 
 /**** <select>, <optgroup> & <option> ****/
 
 testText("<select size='1'><option>abc</option><option>def", "abc\ndef", "<select size='1'> contents of options preserved");
 testText("<select size='2'><option>abc</option><option>def", "abc\ndef", "<select size='2'> contents of options preserved");
 testText("<select size='1'><option id='target'>abc</option><option>def", "abc", "<select size='1'> contents of target option preserved");
--- a/toolkit/components/downloads/DownloadHistory.jsm
+++ b/toolkit/components/downloads/DownloadHistory.jsm
@@ -60,16 +60,18 @@ var DownloadHistory = {
    *        Optional number that limits the amount of results the history query
    *        may return.
    *
    * @return {Promise}
    * @resolves The requested DownloadHistoryList object.
    * @rejects JavaScript exception.
    */
   getList({type = Downloads.PUBLIC, maxHistoryResults} = {}) {
+    DownloadCache.ensureInitialized();
+
     let key = `${type}|${maxHistoryResults ? maxHistoryResults : -1}`;
     if (!this._listPromises[key]) {
       this._listPromises[key] = Downloads.getList(type).then(list => {
         // When the amount of history downloads is capped, we request the list in
         // descending order, to make sure that the list can apply the limit.
         let query = HISTORY_PLACES_QUERY +
           (maxHistoryResults ? "&maxResults=" + maxHistoryResults : "");
         return new DownloadHistoryList(list, query);
@@ -87,43 +89,17 @@ var DownloadHistory = {
   _listPromises: {},
 
   async addDownloadToHistory(download) {
     if (download.source.isPrivate ||
         !PlacesUtils.history.canAddURI(PlacesUtils.toURI(download.source.url))) {
       return;
     }
 
-    let targetFile = new FileUtils.File(download.target.path);
-    let targetUri = Services.io.newFileURI(targetFile);
-
-    let originalPageInfo = await PlacesUtils.history.fetch(download.source.url);
-
-    let pageInfo = await PlacesUtils.history.insert({
-      url: download.source.url,
-      // In case we are downloading a file that does not correspond to a web
-      // page for which the title is present, we populate the otherwise empty
-      // history title with the name of the destination file, to allow it to be
-      // visible and searchable in history results.
-      title: (originalPageInfo && originalPageInfo.title) || targetFile.leafName,
-      visits: [{
-        // The start time is always available when we reach this point.
-        date: download.startTime,
-        transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
-        referrer: download.source.referrer,
-      }]
-    });
-
-    await PlacesUtils.history.update({
-      annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]),
-      // XXX Bug 1479445: We shouldn't have to supply both guid and url here,
-      // but currently we do.
-      guid: pageInfo.guid,
-      url: pageInfo.url,
-    });
+    await DownloadCache.addDownload(download);
 
     await this._updateHistoryListData(download.source.url);
   },
 
   /**
    * Stores new detailed metadata for the given download in history. This is
    * normally called after a download finishes, fails, or is canceled.
    *
@@ -158,110 +134,188 @@ var DownloadHistory = {
     }
 
     // The verdict may still be present even if the download succeeded.
     if (download.error && download.error.reputationCheckVerdict) {
       metaData.reputationCheckVerdict =
         download.error.reputationCheckVerdict;
     }
 
-    try {
-      await PlacesUtils.history.update({
-        annotations: new Map([[METADATA_ANNO, JSON.stringify(metaData)]]),
-        url: download.source.url,
-      });
+    // This should be executed before any async parts, to ensure the cache is
+    // updated before any notifications are activated.
+    await DownloadCache.setMetadata(download.source.url, metaData);
 
-      await this._updateHistoryListData(download.source.url);
-    } catch (ex) {
-      Cu.reportError(ex);
-    }
+    await this._updateHistoryListData(download.source.url);
   },
 
   async _updateHistoryListData(sourceUrl) {
     for (let key of Object.getOwnPropertyNames(this._listPromises)) {
       let downloadHistoryList = await this._listPromises[key];
-      downloadHistoryList.updateForMetadataChange(sourceUrl);
+      downloadHistoryList.updateForMetaDataChange(sourceUrl,
+        DownloadCache.get(sourceUrl));
+    }
+  },
+};
+
+/**
+ * This cache exists:
+ * - in order to optimize the load of DownloadsHistoryList, when Places
+ *   annotations for history downloads must be read. In fact, annotations are
+ *   stored in a single table, and reading all of them at once is much more
+ *   efficient than an individual query.
+ * - to avoid needing to do asynchronous reading of the database during download
+ *   list updates, which are designed to be synchronous (to improve UI
+ *   responsiveness).
+ *
+ * The cache is initialized the first time DownloadHistory.getList is called, or
+ * when data is added.
+ */
+var DownloadCache = {
+  _data: new Map(),
+  _initialized: false,
+
+  /**
+   * Initializes the cache, loading the data from the places database.
+   */
+  ensureInitialized() {
+    if (this._initialized) {
+      return;
+    }
+    this._initialized = true;
+
+    PlacesUtils.history.addObserver(this, true);
+
+    // Read the metadata annotations first, but ignore invalid JSON.
+    for (let result of PlacesUtils.annotations.getAnnotationsWithName(
+                                               METADATA_ANNO)) {
+      try {
+        this._data.set(result.uri.spec, JSON.parse(result.annotationValue));
+      } catch (ex) {}
+    }
+
+    // Add the target file annotations to the metadata.
+    for (let result of PlacesUtils.annotations.getAnnotationsWithName(
+                                               DESTINATIONFILEURI_ANNO)) {
+      let newData = this.get(result.uri.spec);
+      newData.targetFileSpec = result.annotationValue;
+      this._data.set(result.uri.spec, newData);
     }
   },
 
   /**
-   * Reads current metadata from Places annotations for the specified URI, and
-   * returns an object with the format:
+   * This returns an object containing the meta data for the supplied URL.
+   *
+   * @param {String} url The url to get the meta data for.
+   * @return {Object|null} Returns an empty object if there is no meta data found, or
+   *                       an object containing the meta data. The meta data
+   *                       will look like:
    *
    * { targetFileSpec, state, endTime, fileSize, ... }
    *
    * The targetFileSpec property is the value of "downloads/destinationFileURI",
    * while the other properties are taken from "downloads/metaData". Any of the
    * properties may be missing from the object.
    */
-  getPlacesMetaDataFor(spec) {
-    let metaData = {};
+  get(url) {
+    return this._data.get(url) || {};
+  },
+
+  /**
+   * Adds a download to the cache and the places database.
+   *
+   * @param {Download} download The download to add to the database and cache.
+   */
+  async addDownload(download) {
+    this.ensureInitialized();
+
+    let targetFile = new FileUtils.File(download.target.path);
+    let targetUri = Services.io.newFileURI(targetFile);
+
+    // This should be executed before any async parts, to ensure the cache is
+    // updated before any notifications are activated.
+    // Note: this intentionally overwrites any metadata as this is
+    // the start of a new download.
+    this._data.set(download.source.url, { targetFileSpec: targetUri.spec });
+
+    let originalPageInfo = await PlacesUtils.history.fetch(download.source.url);
+
+    let pageInfo = await PlacesUtils.history.insert({
+      url: download.source.url,
+      // In case we are downloading a file that does not correspond to a web
+      // page for which the title is present, we populate the otherwise empty
+      // history title with the name of the destination file, to allow it to be
+      // visible and searchable in history results.
+      title: (originalPageInfo && originalPageInfo.title) || targetFile.leafName,
+      visits: [{
+        // The start time is always available when we reach this point.
+        date: download.startTime,
+        transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
+        referrer: download.source.referrer,
+      }]
+    });
+
+    await PlacesUtils.history.update({
+      annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]),
+      // XXX Bug 1479445: We shouldn't have to supply both guid and url here,
+      // but currently we do.
+      guid: pageInfo.guid,
+      url: pageInfo.url,
+    });
+  },
+
+  /**
+   * Sets the metadata for a given url. If the cache already contains meta data
+   * for the given url, it will be overwritten (note: the targetFileSpec will be
+   * maintained).
+   *
+   * @param {String} url The url to set the meta data for.
+   * @param {Object} metadata The new metaData to save in the cache.
+   */
+  async setMetadata(url, metadata) {
+    this.ensureInitialized();
+
+    // This should be executed before any async parts, to ensure the cache is
+    // updated before any notifications are activated.
+    let existingData = this.get(url);
+    let newData = { ...metadata };
+    if ("targetFileSpec" in existingData) {
+      newData.targetFileSpec = existingData.targetFileSpec;
+    }
+    this._data.set(url, newData);
 
     try {
-      let uri = Services.io.newURI(spec);
-      try {
-        metaData = JSON.parse(PlacesUtils.annotations.getPageAnnotation(
-                                          uri, METADATA_ANNO));
-      } catch (ex) {}
-      metaData.targetFileSpec = PlacesUtils.annotations.getPageAnnotation(
-                                            uri, DESTINATIONFILEURI_ANNO);
-    } catch (ex) {}
+      await PlacesUtils.history.update({
+        annotations: new Map([[METADATA_ANNO, JSON.stringify(metadata)]]),
+        url,
+      });
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+  },
 
-    return metaData;
-  },
-};
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsINavHistoryObserver,
+    Ci.nsISupportsWeakReference
+  ]),
 
-/**
- * This cache exists in order to optimize the load of DownloadsHistoryList, when
- * Places annotations for history downloads must be read. In fact, annotations
- * are stored in a single table, and reading all of them at once is much more
- * efficient than an individual query.
- *
- * When this property is first requested, it reads the annotations for all the
- * history downloads and stores them indefinitely.
- *
- * The historical annotations are not expected to change for the duration of the
- * session, except in the case where a session download is running for the same
- * URI as a history download. To avoid using stale data, consumers should
- * permanently remove from the cache any URI corresponding to a session
- * download. This is a very small mumber compared to history downloads.
- *
- * This property returns a Map from each download source URI found in Places
- * annotations to an object with the format:
- *
- * { targetFileSpec, state, endTime, fileSize, ... }
- *
- * The targetFileSpec property is the value of "downloads/destinationFileURI",
- * while the other properties are taken from "downloads/metaData". Any of the
- * properties may be missing from the object.
- */
-XPCOMUtils.defineLazyGetter(this, "gCachedPlacesMetaData", function() {
-  let placesMetaData = new Map();
-
-  // Read the metadata annotations first, but ignore invalid JSON.
-  for (let result of PlacesUtils.annotations.getAnnotationsWithName(
-                                             METADATA_ANNO)) {
-    try {
-      placesMetaData.set(result.uri.spec, JSON.parse(result.annotationValue));
-    } catch (ex) {}
-  }
-
-  // Add the target file annotations to the metadata.
-  for (let result of PlacesUtils.annotations.getAnnotationsWithName(
-                                             DESTINATIONFILEURI_ANNO)) {
-    let metaData = placesMetaData.get(result.uri.spec);
-    if (!metaData) {
-      metaData = {};
-      placesMetaData.set(result.uri.spec, metaData);
-    }
-    metaData.targetFileSpec = result.annotationValue;
-  }
-
-  return placesMetaData;
-});
+  // nsINavHistoryObserver
+  onDeleteURI(uri) {
+    this._data.delete(uri.spec);
+  },
+  onClearHistory() {
+    this._data.clear();
+  },
+  onBeginUpdateBatch() {},
+  onEndUpdateBatch() {},
+  onTitleChanged() {},
+  onFrecencyChanged() {},
+  onManyFrecenciesChanged() {},
+  onPageChanged() {},
+  onDeleteVisits() {},
+};
 
 /**
  * Represents a download from the browser history. This object implements part
  * of the interface of the Download object.
  *
  * While Download objects are shared between the public DownloadList and all the
  * DownloadHistoryList instances, multiple HistoryDownload objects referring to
  * the same item can be created for different DownloadHistoryList instances.
@@ -505,30 +559,30 @@ this.DownloadHistoryList.prototype = {
   },
   _result: null,
 
   /**
    * Updates the download history item when the meta data or destination file
    * changes.
    *
    * @param {String} sourceUrl The sourceUrl which was updated.
+   * @param {Object} metaData The new meta data for the sourceUrl.
    */
-  updateForMetadataChange(sourceUrl) {
+  updateForMetaDataChange(sourceUrl, metaData) {
     let slotsForUrl = this._slotsForUrl.get(sourceUrl);
     if (!slotsForUrl) {
       return;
     }
 
     for (let slot of slotsForUrl) {
       if (slot.sessionDownload) {
         // The visible data doesn't change, so we don't have to notify views.
         return;
       }
-      slot.historyDownload.updateFromMetaData(
-        DownloadHistory.getPlacesMetaDataFor(sourceUrl));
+      slot.historyDownload.updateFromMetaData(metaData);
       this._notifyAllViews("onDownloadChanged", slot.download);
     }
   },
 
   /**
    * Index of the first slot that contains a session download. This is equal to
    * the length of the list when there are no session downloads.
    */
@@ -597,19 +651,17 @@ this.DownloadHistoryList.prototype = {
       }
       return;
     }
 
     // If there are no existing slots for this URL, we have to create a new one.
     // Since the history download is visible in the slot, we also have to update
     // the object using the Places metadata.
     let historyDownload = new HistoryDownload(placesNode);
-    historyDownload.updateFromMetaData(
-      gCachedPlacesMetaData.get(placesNode.uri) ||
-      DownloadHistory.getPlacesMetaDataFor(placesNode.uri));
+    historyDownload.updateFromMetaData(DownloadCache.get(placesNode.uri));
     let slot = new DownloadSlot(this);
     slot.historyDownload = historyDownload;
     this._insertSlot({ slot, slotsForUrl, index: this._firstSessionSlotIndex });
   },
 
   // nsINavHistoryResultObserver
   containerStateChanged(node, oldState, newState) {
     this.invalidateContainer(node);
@@ -675,26 +727,16 @@ this.DownloadHistoryList.prototype = {
   nodeURIChanged() {},
   batching() {},
 
   // DownloadList callback
   onDownloadAdded(download) {
     let url = download.source.url;
     let slotsForUrl = this._slotsForUrl.get(url) || new Set();
 
-    // When a session download is attached to a slot, we ensure not to keep
-    // stale metadata around for the corresponding history download. This
-    // prevents stale state from being used if the view is rebuilt.
-    //
-    // Note that we will eagerly load the data in the cache at this point, even
-    // if we have seen no history download. The case where no history download
-    // will appear at all is rare enough in normal usage, so we can apply this
-    // simpler solution rather than keeping a list of cache items to ignore.
-    gCachedPlacesMetaData.delete(url);
-
     // For every source URL, there can be at most one slot containing a history
     // download without an associated session download. If we find one, then we
     // can reuse it for the current session download, although we have to move
     // it together with the other session downloads.
     let slot = [...slotsForUrl][0];
     if (slot && !slot.sessionDownload) {
       // Remove the slot because we have to change its position.
       this._removeSlot({ slot, slotsForUrl });
@@ -714,33 +756,33 @@ this.DownloadHistoryList.prototype = {
 
   // DownloadList callback
   onDownloadRemoved(download) {
     let url = download.source.url;
     let slotsForUrl = this._slotsForUrl.get(url);
     let slot = this._slotForDownload.get(download);
     this._removeSlot({ slot, slotsForUrl });
 
+    this._slotForDownload.delete(download);
+
     // If there was only one slot for this source URL and it also contained a
     // history download, we should resurrect it in the correct area of the list.
     if (slotsForUrl.size == 0 && slot.historyDownload) {
       // We have one download slot containing both a session download and a
       // history download, and we are now removing the session download.
       // Previously, we did not use the Places metadata because it was obscured
       // by the session download. Since this is no longer the case, we have to
       // read the latest metadata before resurrecting the history download.
       slot.historyDownload.updateFromMetaData(
-        DownloadHistory.getPlacesMetaDataFor(url));
+        DownloadCache.get(url));
       slot.sessionDownload = null;
       // Place the resurrected history slot after all the session slots.
       this._insertSlot({ slot, slotsForUrl,
                          index: this._firstSessionSlotIndex });
     }
-
-    this._slotForDownload.delete(download);
   },
 
   // DownloadList
   add() {
     throw new Error("Not implemented.");
   },
 
   // DownloadList
new file mode 100644
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/test_DownloadHistory_initialization.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/DownloadHistory.jsm");
+ChromeUtils.import("resource://testing-common/PlacesTestUtils.jsm");
+
+let baseDate = new Date("2000-01-01");
+
+/**
+ * This test is designed to ensure the cache of download history is correctly
+ * loaded and initialized. We do this by having the test as the only test in
+ * this file, and injecting data into the places database before we start.
+ */
+add_task(async function test_DownloadHistory_initialization() {
+  // Clean up at the beginning and at the end of the test.
+  async function cleanup() {
+    await PlacesUtils.history.clear();
+  }
+  registerCleanupFunction(cleanup);
+  await cleanup();
+
+  let testDownloads = [];
+  for (let i = 10; i <= 30; i += 10) {
+    let targetFile = getTempFile(`${TEST_TARGET_FILE_NAME}${i}`);
+    let download = {
+      source: {
+        url: httpUrl(`source${i}`),
+        isPrivate: false
+      },
+      target: { path: targetFile.path },
+      endTime: baseDate.getTime() + i,
+      fileSize: 100 + i,
+      state: i / 10,
+    };
+
+    await PlacesTestUtils.addVisits([{
+      uri: download.source.url,
+      transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
+    }]);
+
+    let targetUri = Services.io.newFileURI(new FileUtils.File(download.target.path));
+
+    await PlacesUtils.history.update({
+      annotations: new Map([
+        [ "downloads/destinationFileURI", targetUri.spec ],
+        [ "downloads/metaData", JSON.stringify({
+          state: download.state,
+          endTime: download.endTime,
+          fileSize: download.fileSize,
+        })]
+      ]),
+      url: download.source.url,
+    });
+
+    testDownloads.push(download);
+  }
+
+  // Initialize DownloadHistoryList only after having added the history and
+  // session downloads.
+  let historyList = await DownloadHistory.getList();
+  let downloads = await historyList.getAll();
+  Assert.equal(downloads.length, testDownloads.length);
+
+  for (let expected of testDownloads) {
+    let download = downloads.find(d => d.source.url == expected.source.url);
+
+    info(`Checking download ${expected.source.url}`);
+    Assert.ok(download, "Should have found the expected download");
+    Assert.equal(download.endTime, expected.endTime,
+      "Should have the correct end time");
+    Assert.equal(download.target.size, expected.fileSize,
+      "Should have the correct file size");
+    Assert.equal(download.succeeded, expected.state == 1,
+      "Should have the correct succeeded value");
+    Assert.equal(download.canceled, expected.state == 3,
+      "Should have the correct canceled value");
+    Assert.equal(download.target.path, expected.target.path,
+      "Should have the correct target path");
+  }
+});
--- a/toolkit/components/downloads/test/unit/xpcshell.ini
+++ b/toolkit/components/downloads/test/unit/xpcshell.ini
@@ -4,16 +4,17 @@ skip-if = toolkit == 'android'
 
 # Note: The "tail.js" file is not defined in the "tail" key because it calls
 #       the "add_test_task" function, that does not work properly in tail files.
 support-files =
   common_test_Download.js
 
 [test_DownloadCore.js]
 [test_DownloadHistory.js]
+[test_DownloadHistory_initialization.js]
 [test_DownloadIntegration.js]
 [test_DownloadLegacy.js]
 [test_DownloadList.js]
 [test_DownloadPaths.js]
 [test_Downloads.js]
 [test_DownloadStore.js]
 skip-if = (verify && !debug && (os == 'linux'))
 [test_PrivateTemp.js]