Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
authorCiure Andrei <aciure@mozilla.com>
Wed, 03 Oct 2018 19:46:59 +0300
changeset 495188 0db7d36b0c4ad1ea4aefade660437a0e2305cd51
parent 495187 984902ba6faf1864c04fef525f6764983439ce1b (current diff)
parent 495121 3530790e23d18b6f8f73471e367a942f201dd452 (diff)
child 495189 bd37a0888c5220dd112f9a887951303aa2389a41
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
--- a/devtools/server/actors/accessibility.js
+++ b/devtools/server/actors/accessibility.js
@@ -16,16 +16,17 @@ const {
   accessibleWalkerSpec,
   accessibilitySpec
 } = require("devtools/shared/specs/accessibility");
 
 const { isXUL } = require("devtools/server/actors/highlighters/utils/markup");
 const { isWindowIncluded } = require("devtools/shared/layout/utils");
 const { CustomHighlighterActor, register } =
   require("devtools/server/actors/highlighters");
+const { getContrastRatioFor } = require("devtools/server/actors/utils/accessibility");
 const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
 
 const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
 const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
 const nsIAccessibleRole = Ci.nsIAccessibleRole;
 
 const {
   EVENT_TEXT_CHANGED,
@@ -363,16 +364,46 @@ const AccessibleActor = ActorClassWithSp
       keyboardShortcut: this.keyboardShortcut,
       childCount: this.childCount,
       domNodeType: this.domNodeType,
       indexInParent: this.indexInParent,
       states: this.states,
       actions: this.actions,
       attributes: this.attributes
     };
+  },
+
+  _isValidTextLeaf(rawAccessible) {
+    return !isDefunct(rawAccessible) &&
+           rawAccessible.role === nsIAccessibleRole.ROLE_TEXT_LEAF &&
+           rawAccessible.name && rawAccessible.name.trim().length > 0;
+  },
+
+  get _nonEmptyTextLeafs() {
+    return this.children().filter(child => this._isValidTextLeaf(child.rawAccessible));
+  },
+
+  /**
+   * Calculate the contrast ratio of the given accessible.
+   */
+  _getContrastRatio() {
+    return getContrastRatioFor(this._isValidTextLeaf(this.rawAccessible) ?
+      this.rawAccessible.DOMNode.parentNode : this.rawAccessible.DOMNode);
+  },
+
+  /**
+   * Audit the state of the accessible object.
+   *
+   * @return {Object|null}
+   *         Audit results for the accessible object.
+  */
+  get audit() {
+    return this.isDefunct ? null : {
+      contrastRatio: this._getContrastRatio()
+    };
   }
 });
 
 /**
  * The AccessibleWalkerActor stores a cache of AccessibleActors that represent
  * accessible objects in a given document.
  *
  * It is also responsible for implicitely initializing and shutting down
@@ -705,23 +736,24 @@ const AccessibleWalkerActor = ActorClass
    * @param  {Object} options
    *         Object used for passing options. Available options:
    *         - duration {Number}
    *                    Duration of time that the highlighter should be shown.
    * @return {Boolean}
    *         True if highlighter shows the accessible object.
    */
   highlightAccessible(accessible, options = {}) {
-    const { bounds, name, role } = accessible;
+    const { bounds } = accessible;
     if (!bounds) {
       return false;
     }
 
+    const { audit, name, role } = accessible;
     return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
-                                 { ...options, ...bounds, name, role });
+                                 { ...options, ...bounds, name, role, audit });
   },
 
   /**
    * Public method used to hide an accessible object highlighter on the client
    * side.
    */
   unhighlight() {
     this.highlighter.hide();
@@ -798,25 +830,17 @@ const AccessibleWalkerActor = ActorClass
     }
 
     const accessible = await this._findAndAttachAccessible(event);
     if (!accessible) {
       return;
     }
 
     if (this._currentAccessible !== accessible) {
-      const { bounds, role, name } = accessible;
-      if (bounds) {
-        this.highlighter.show({ rawNode: event.originalTarget || event.target }, {
-          ...bounds,
-          role,
-          name
-        });
-      }
-
+      this.highlightAccessible(accessible);
       events.emit(this, "picker-accessible-hovered", accessible);
       this._currentAccessible = accessible;
     }
   },
 
   /**
    * Keyboard event handler for when picking is enabled.
    *
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -642,23 +642,42 @@
 
 /* Accessible highlighter */
 
 :-moz-native-anonymous .accessible-bounds {
   opacity: 0.6;
   fill: #6a5acd;
 }
 
-:-moz-native-anonymous .accessible-infobar-name {
-  color:var(--highlighter-infobar-color);
+:-moz-native-anonymous .accessible-infobar-name,
+:-moz-native-anonymous .accessible-infobar-audit {
+  color: var(--highlighter-infobar-color);
   max-width: 90%;
 }
 
-:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
-  color: var(--highlighter-infobar-color);
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
+  color: #90E274;
+}
+
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
+  color: #E57180;
+  content: " ⚠️";
+}
+
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
+  content: " AA\2713";
+}
+
+:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
+  content: " AAA\2713";
+}
+
+:-moz-native-anonymous .accessible-infobar-name:not(:empty),
+:-moz-native-anonymous .accessible-infobar-audit:not(:empty) {
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
 
 :-moz-native-anonymous .accessible-infobar-role {
   color: #9CDCFE;
 }
--- a/devtools/server/actors/highlighters/utils/accessibility.js
+++ b/devtools/server/actors/highlighters/utils/accessibility.js
@@ -1,29 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layout/utils");
 const { moveInfobar, createNode } = require("./markup");
 const { truncateString } = require("devtools/shared/inspector/utils");
 
+const STRINGS_URI = "devtools/shared/locales/accessibility.properties";
+loader.lazyRequireGetter(this, "LocalizationHelper", "devtools/shared/l10n", true);
+DevToolsUtils.defineLazyGetter(this, "L10N", () => new LocalizationHelper(STRINGS_URI));
+
 // Max string length for truncating accessible name values.
 const MAX_STRING_LENGTH = 50;
 
 /**
  * The AccessibleInfobar is a class responsible for creating the markup for the
  * accessible highlighter. It is also reponsible for updating content within the
  * infobar such as role and name values.
  */
 class Infobar {
   constructor(highlighter) {
     this.highlighter = highlighter;
+    this.audit = new Audit(this);
   }
 
   get document() {
     return this.highlighter.win.document;
   }
 
   get bounds() {
     return this.highlighter._bounds;
@@ -105,23 +111,27 @@ class Infobar {
       nodeType: "span",
       parent: infobarText,
       attributes: {
         "class": "infobar-name",
         "id": "infobar-name",
       },
       prefix: this.prefix,
     });
+
+    this.audit.buildMarkup(infobarText);
   }
 
   /**
    * Destroy the Infobar's highlighter.
    */
   destroy() {
     this.highlighter = null;
+    this.audit.destroy();
+    this.audit = null;
   }
 
   /**
    * Gets the element with the specified ID.
    *
    * @param  {String} id
    *         Element ID.
    * @return {Element} The element with specified ID.
@@ -163,20 +173,21 @@ class Infobar {
     // Update the infobar's position and content.
     this.update(container);
   }
 
   /**
    * Update content of the infobar.
    */
   update(container) {
-    const { name, role } = this.options;
+    const { audit, name, role } = this.options;
 
     this.updateRole(role, this.getElement("infobar-role"));
     this.updateName(name, this.getElement("infobar-name"));
+    this.audit.update(audit);
 
     // Position the infobar.
     this._moveInfobar(container);
   }
 
   /**
    * Sets the text content of the specified element.
    *
@@ -351,16 +362,155 @@ class XULWindowInfobar extends Infobar {
    *         Text for content.
    */
   setTextContent(el, text) {
     el.textContent = text;
   }
 }
 
 /**
+ * Audit component used within the accessible highlighter infobar. This component is
+ * responsible for rendering and updating its containing AuditReport components that
+ * display various audit information such as contrast ratio score.
+ */
+class Audit {
+  constructor(infobar) {
+    this.infobar = infobar;
+
+    // A list of audit reports to be shown on the fly when highlighting an accessible
+    // object.
+    this.reports = [
+      new ContrastRatio(this)
+    ];
+  }
+
+  get prefix() {
+    return this.infobar.prefix;
+  }
+
+  get win() {
+    return this.infobar.win;
+  }
+
+  buildMarkup(root) {
+    const audit = createNode(this.win, {
+      nodeType: "span",
+      parent: root,
+      attributes: {
+        "class": "infobar-audit",
+        "id": "infobar-audit",
+      },
+      prefix: this.prefix,
+    });
+
+    this.reports.forEach(report => report.buildMarkup(audit));
+  }
+
+  update(audit = {}) {
+    const el = this.getElement("infobar-audit");
+    el.setAttribute("hidden", true);
+
+    let updated = false;
+    this.reports.forEach(report => {
+      if (report.update(audit)) {
+        updated = true;
+      }
+    });
+
+    if (updated) {
+      el.removeAttribute("hidden");
+    }
+  }
+
+  getElement(id) {
+    return this.infobar.getElement(id);
+  }
+
+  setTextContent(el, text) {
+    return this.infobar.setTextContent(el, text);
+  }
+
+  destroy() {
+    this.infobar = null;
+    this.reports.forEach(report => report.destroy());
+    this.reports = null;
+  }
+}
+
+/**
+ * A common interface between audit report components used to render accessibility audit
+ * information for the currently highlighted accessible object.
+ */
+class AuditReport {
+  constructor(audit) {
+    this.audit = audit;
+  }
+
+  get prefix() {
+    return this.audit.prefix;
+  }
+
+  get win() {
+    return this.audit.win;
+  }
+
+  getElement(id) {
+    return this.audit.getElement(id);
+  }
+
+  setTextContent(el, text) {
+    return this.audit.setTextContent(el, text);
+  }
+
+  destroy() {
+    this.audit = null;
+  }
+}
+
+/**
+ * Contrast ratio audit report that is used to display contrast ratio score as part of the
+ * inforbar,
+ */
+class ContrastRatio extends AuditReport {
+  buildMarkup(root) {
+    createNode(this.win, {
+      nodeType: "span",
+      parent: root,
+      attributes: {
+        "class": "contrast-ratio",
+        "id": "contrast-ratio",
+      },
+      prefix: this.prefix,
+    });
+  }
+
+  /**
+   * Update contrast ratio score infobar markup.
+   * @param  {Number}
+   *         Contrast ratio for an accessible object being highlighted.
+   * @return {Boolean}
+   *         True if the contrast ratio markup was updated correctly and infobar audit
+   *         block should be visible.
+   */
+  update({ contrastRatio }) {
+    const el = this.getElement("contrast-ratio");
+    ["fail", "AA", "AAA"].forEach(style => el.classList.remove(style));
+
+    if (!contrastRatio) {
+      return false;
+    }
+
+    el.classList.add(getContrastRatioScoreStyle(contrastRatio));
+    this.setTextContent(el,
+      L10N.getFormatStr("accessibility.contrast.ratio", contrastRatio.ratio.toFixed(2)));
+    return true;
+  }
+}
+
+/**
  * A helper function that calculate accessible object bounds and positioning to
  * be used for highlighting.
  *
  * @param  {Object} win
  *         window that contains accessible object.
  * @param  {Object} options
  *         Object used for passing options:
  *         - {Number} x
@@ -405,12 +555,35 @@ function getBounds(win, { x, y, w, h, zo
   bottom *= zoomFactor;
 
   const width = right - left;
   const height = bottom - top;
 
   return { left, right, top, bottom, width, height };
 }
 
+/**
+ * Get contrast ratio score styling to be applied on the element that renders the contrast
+ * ratio.
+ * @param  {Number} options.ratio
+ *         Value of the contrast ratio for a given accessible object.
+ * @param  {Boolean} options.largeText
+ *         True if the accessible object contains large text.
+ * @return {String}
+ *         CSS class that represents the appropriate contrast ratio score styling.
+ */
+function getContrastRatioScoreStyle({ ratio, largeText }) {
+  const levels = largeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
+
+  let style = "fail";
+  if (ratio >= levels.AAA) {
+    style = "AAA";
+  } else if (ratio >= levels.AA) {
+    style = "AA";
+  }
+
+  return style;
+}
+
 exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
 exports.getBounds = getBounds;
 exports.Infobar = Infobar;
 exports.XULWindowInfobar = XULWindowInfobar;
--- a/devtools/server/actors/highlighters/xul-accessible.js
+++ b/devtools/server/actors/highlighters/xul-accessible.js
@@ -68,23 +68,42 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:te
 
   .accessible-infobar-text {
     overflow: hidden;
     white-space: nowrap;
     display: flex;
     justify-content: center;
   }
 
-  .accessible-infobar-name {
-    color: rgb(221, 0, 169);
+  .accessible-infobar-name,
+  .accessible-infobar-audit {
+    color: hsl(210, 30%, 85%);
     max-width: 90%;
   }
 
-  .accessible-infobar-name:not(:empty) {
-    color: hsl(210, 30%, 85%);
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
+    color: #90E274;
+  }
+
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
+    color: #E57180;
+    content: " ⚠️";
+  }
+
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
+    content: " AA\u2713";
+  }
+
+  .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
+    content: " AAA\u2713";
+  }
+
+  .accessible-infobar-name:not(:empty),
+  .accessible-infobar-audit:not(:empty) {
     border-inline-start: 1px solid #5a6169;
     margin-inline-start: 6px;
     padding-inline-start: 6px;
   }
 
   .accessible-infobar-role {
     color: #9CDCFE;
   }`);
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -5,18 +5,16 @@
 "use strict";
 
 const { Cu } = require("chrome");
 const Services = require("Services");
 const InspectorUtils = require("InspectorUtils");
 const protocol = require("devtools/shared/protocol");
 const { nodeSpec, nodeListSpec } = require("devtools/shared/specs/node");
 
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-
 loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 
 loader.lazyRequireGetter(this, "isAfterPseudoElement", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isBeforePseudoElement", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isDirectShadowHostChild", "devtools/shared/layout/utils", true);
@@ -681,36 +679,25 @@ const NodeActor = protocol.ActorClassWit
       fillStyle: fillStyle
     };
     const { dataURL, size } = getFontPreviewData(font, doc, options);
 
     return { data: LongStringActor(this.conn, dataURL), size: size };
   },
 
   /**
-   * Finds the computed background color of the closest parent with
-   * a set background color.
-   * Returns a string with the background color of the form
-   * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
-   * background color is found.
+   * Finds the computed background color of the closest parent with a set background
+   * color.
+   *
+   * @return {String}
+   *         String with the background color of the form rgba(r, g, b, a). Defaults to
+   *         rgba(255, 255, 255, 1) if no background color is found.
    */
   getClosestBackgroundColor: function() {
-    let current = this.rawNode;
-    while (current) {
-      const computedStyle = CssLogic.getComputedStyle(current);
-      const currentStyle = computedStyle.getPropertyValue("background-color");
-      if (colorUtils.isValidCSSColor(currentStyle)) {
-        const currentCssColor = new colorUtils.CssColor(currentStyle);
-        if (!currentCssColor.isTransparent()) {
-          return currentCssColor.rgba;
-        }
-      }
-      current = current.parentNode;
-    }
-    return "rgba(255, 255, 255, 1)";
+    return InspectorActorUtils.getClosestBackgroundColor(this.rawNode);
   },
 
   /**
    * Returns an object with the width and height of the node's owner window.
    *
    * @return {Object}
    */
   getOwnerGlobalDimensions: function() {
--- a/devtools/server/actors/inspector/utils.js
+++ b/devtools/server/actors/inspector/utils.js
@@ -1,24 +1,27 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cu} = require("chrome");
 
+loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
 loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
 loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
 loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
 loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
 
 loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
 
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
+
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const IMAGE_FETCHING_TIMEOUT = 500;
 
 /**
  * Returns the properly cased version of the node's tag name, which can be
  * used when displaying said name in the UI.
  *
@@ -240,16 +243,75 @@ const imageToImageData = async function(
     size: {
       naturalWidth: imgWidth,
       naturalHeight: imgHeight,
       resized: resizeRatio !== 1
     }
   };
 };
 
+/**
+ * Finds the computed background color of the closest parent with a set background color.
+ *
+ * @param  {DOMNode}  node
+ *         Node for which we want to find closest background color.
+ * @return {String}
+ *         String with the background color of the form rgba(r, g, b, a). Defaults to
+ *         rgba(255, 255, 255, 1) if no background color is found.
+ */
+function getClosestBackgroundColor(node) {
+  let current = node;
+
+  while (current) {
+    const computedStyle = CssLogic.getComputedStyle(current);
+    if (computedStyle) {
+      const currentStyle = computedStyle.getPropertyValue("background-color");
+      if (colorUtils.isValidCSSColor(currentStyle)) {
+        const currentCssColor = new colorUtils.CssColor(currentStyle);
+        if (!currentCssColor.isTransparent()) {
+          return currentCssColor.rgba;
+        }
+      }
+    }
+
+    current = current.parentNode;
+  }
+
+  return "rgba(255, 255, 255, 1)";
+}
+
+/**
+ * Finds the background image of the closest parent where it is set.
+ *
+ * @param  {DOMNode}  node
+ *         Node for which we want to find the background image.
+ * @return {String}
+ *         String with the value of the background iamge property. Defaults to "none" if
+ *         no background image is found.
+ */
+function getClosestBackgroundImage(node) {
+  let current = node;
+
+  while (current.ownerDocument) {
+    const computedStyle = CssLogic.getComputedStyle(current);
+    if (computedStyle) {
+      const currentBackgroundImage = computedStyle.getPropertyValue("background-image");
+      if (currentBackgroundImage !== "none") {
+        return currentBackgroundImage;
+      }
+    }
+
+    current = current.parentNode;
+  }
+
+  return "none";
+}
+
 module.exports = {
   allAnonymousContentTreeWalkerFilter,
+  getClosestBackgroundColor,
+  getClosestBackgroundImage,
   getNodeDisplayName,
   imageToImageData,
   isNodeDead,
   nodeDocument,
   standardTreeWalkerFilter,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/utils/accessibility.js
@@ -0,0 +1,65 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
+loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
+
+/**
+ * Calculates the contrast ratio of the referenced DOM node.
+ *
+ * @param  {DOMNode} node
+ *         The node for which we want to calculate the contrast ratio.
+ *
+ * @return {Number|null} Contrast ratio value.
+*/
+function getContrastRatioFor(node) {
+  const backgroundColor = InspectorActorUtils.getClosestBackgroundColor(node);
+  const backgroundImage = InspectorActorUtils.getClosestBackgroundImage(node);
+  const computedStyles = CssLogic.getComputedStyle(node);
+  if (!computedStyles) {
+    return null;
+  }
+
+  const { color, "font-size": fontSize, "font-weight": fontWeight } = computedStyles;
+  const isBoldText = parseInt(fontWeight, 10) >= 600;
+  const backgroundRgbaColor = new colorUtils.CssColor(backgroundColor, true);
+  const textRgbaColor = new colorUtils.CssColor(color, true);
+
+  // TODO: For cases where text color is transparent, it likely comes from the color of
+  // the background that is underneath it (commonly from background-clip: text property).
+  // With some additional investigation it might be possible to calculate the color
+  // contrast where the color of the background is used as text color and the color of
+  // the ancestor's background is used as its background.
+  if (textRgbaColor.isTransparent()) {
+    return null;
+  }
+
+  // TODO: these cases include handling gradient backgrounds and the actual image
+  // backgrounds. Each one needs to be handled individually.
+  if (backgroundImage !== "none") {
+    return null;
+  }
+
+  let { r: bgR, g: bgG, b: bgB, a: bgA} = backgroundRgbaColor.getRGBATuple();
+  let { r: textR, g: textG, b: textB, a: textA } = textRgbaColor.getRGBATuple();
+
+  // If the element has opacity in addition to text and background alpha values, take it
+  // into account.
+  const opacity = parseFloat(computedStyles.opacity);
+  if (opacity < 1) {
+    bgA = opacity * bgA;
+    textA = opacity * textA;
+  }
+
+  return {
+    ratio: colorUtils.calculateContrastRatio([ bgR, bgG, bgB, bgA ],
+                                             [ textR, textG, textB, textA ]),
+    largeText: Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18)
+  };
+}
+
+exports.getContrastRatioFor = getContrastRatioFor;
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'accessibility.js',
     'actor-registry-utils.js',
     'actor-registry.js',
     'audionodes.json',
     'automation-timeline.js',
     'breakpoint-actor-map.js',
     'call-watcher.js',
     'css-grid-utils.js',
     'event-loop.js',
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -1158,24 +1158,52 @@ function calculateLuminance(rgba) {
     rgba[i] /= 255;
     rgba[i] = (rgba[i] < 0.03928) ? (rgba[i] / 12.92) :
                                     Math.pow(((rgba[i] + 0.055) / 1.055), 2.4);
   }
   return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];
 }
 
 /**
+ * Blend background and foreground colors takign alpha into account.
+ * @param  {Array} foregroundColor
+ *         An array with [r,g,b,a] values containing the foreground color.
+ * @param  {Array} backgroundColor
+ *         An array with [r,g,b,a] values containing the background color. Defaults to
+ *         [ 255, 255, 255, 1 ].
+ * @return {Array}
+ *         An array with combined [r,g,b,a] colors.
+ */
+function blendColors(foregroundColor, backgroundColor = [ 255, 255, 255, 1 ]) {
+  const [ fgR, fgG, fgB, fgA ] = foregroundColor;
+  const [ bgR, bgG, bgB, bgA ] = backgroundColor;
+  if (fgA === 1) {
+    return foregroundColor;
+  }
+
+  return [
+    (1 - fgA) * bgR + fgA * fgR,
+    (1 - fgA) * bgG + fgA * fgG,
+    (1 - fgA) * bgB + fgA * fgB,
+    fgA + bgA * (1 - fgA)
+  ];
+}
+
+/**
  * Calculates the contrast ratio of 2 rgba tuples based on the formula in
  * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
  *
  * @param {Array} backgroundColor An array with [r,g,b,a] values containing
  * the background color.
  * @param {Array} textColor An array with [r,g,b,a] values containing
  * the text color.
  * @return {Number} The calculated luminance.
  */
 function calculateContrastRatio(backgroundColor, textColor) {
+  backgroundColor = blendColors(backgroundColor);
+  textColor = blendColors(textColor, backgroundColor);
+
   const backgroundLuminance = calculateLuminance(backgroundColor);
   const textLuminance = calculateLuminance(textColor);
   const ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05);
 
   return (ratio > 1.0) ? ratio : (1 / ratio);
 }
new file mode 100644
--- /dev/null
+++ b/devtools/shared/locales/en-US/accessibility.properties
@@ -0,0 +1,8 @@
+# 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/.
+
+# LOCALIZATION NOTE (accessibility.contrast.ratio): A title text for the color contrast
+# ratio description, used by the accessibility highlighter to display the value. %S in the
+# content will be replaced by the contrast ratio numerical value.
+accessibility.contrast.ratio=Contrast: %S
--- a/dom/base/nsRange.cpp
+++ b/dom/base/nsRange.cpp
@@ -3452,27 +3452,16 @@ IsVisibleAndNotInReplacedElement(nsIFram
         !f->GetContent()->IsHTMLElement(nsGkAtoms::button) &&
         !f->GetContent()->IsHTMLElement(nsGkAtoms::select)) {
       return false;
     }
   }
   return true;
 }
 
-static bool
-ElementIsVisibleNoFlush(Element* aElement)
-{
-  if (!aElement) {
-    return false;
-  }
-  RefPtr<ComputedStyle> sc =
-    nsComputedDOMStyle::GetComputedStyleNoFlush(aElement, nullptr);
-  return sc && sc->StyleVisibility()->IsVisible();
-}
-
 static void
 AppendTransformedText(InnerTextAccumulator& aResult, nsIContent* aContainer)
 {
   auto textNode = static_cast<CharacterData*>(aContainer);
 
   nsIFrame* frame = textNode->GetPrimaryFrame();
   if (!IsVisibleAndNotInReplacedElement(frame)) {
     return;
@@ -3578,22 +3567,17 @@ nsRange::GetInnerTextNoFlush(DOMString& 
     currentState = AT_NODE;
   }
 
   while (currentNode != endNode || currentState != endState) {
     nsIFrame* f = currentNode->GetPrimaryFrame();
     bool isVisibleAndNotReplaced = IsVisibleAndNotInReplacedElement(f);
     if (currentState == AT_NODE) {
       bool isText = currentNode->IsText();
-      if (isText && currentNode->GetParent()->IsHTMLElement(nsGkAtoms::rp) &&
-          ElementIsVisibleNoFlush(currentNode->GetParent()->AsElement())) {
-        nsAutoString str;
-        currentNode->GetTextContent(str, aError);
-        result.Append(str);
-      } else if (isVisibleAndNotReplaced) {
+      if (isVisibleAndNotReplaced) {
         result.AddRequiredLineBreakCount(GetRequiredInnerTextLineBreakCount(f));
         if (isText) {
           nsIFrame::RenderedText text = f->GetRenderedText();
           result.Append(text.mString);
         }
       }
       nsIContent* child = currentNode->GetFirstChild();
       if (child) {
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -1145,25 +1145,31 @@ MediaFormatReader::Shutdown()
   }
   if (mVideo.HasPromise()) {
     mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (HasAudio()) {
     mAudio.ResetDemuxer();
     mAudio.mTrackDemuxer->BreakCycles();
-    mAudio.mTrackDemuxer = nullptr;
+    {
+      MutexAutoLock lock(mAudio.mMutex);
+      mAudio.mTrackDemuxer = nullptr;
+    }
     mAudio.ResetState();
     ShutdownDecoder(TrackInfo::kAudioTrack);
   }
 
   if (HasVideo()) {
     mVideo.ResetDemuxer();
     mVideo.mTrackDemuxer->BreakCycles();
-    mVideo.mTrackDemuxer = nullptr;
+    {
+      MutexAutoLock lock(mVideo.mMutex);
+      mVideo.mTrackDemuxer = nullptr;
+    }
     mVideo.ResetState();
     ShutdownDecoder(TrackInfo::kVideoTrack);
   }
 
   mShutdownPromisePool->Track(mDemuxer->Shutdown());
   mDemuxer = nullptr;
 
   mOnTrackWaitingForKeyListener.Disconnect();
@@ -1382,65 +1388,63 @@ MediaFormatReader::OnDemuxerInitDone(con
   }
 
   // To decode, we need valid video and a place to put it.
   bool videoActive =
     !!mDemuxer->GetNumberTracks(TrackInfo::kVideoTrack) && GetImageContainer();
 
   if (videoActive) {
     // We currently only handle the first video track.
+    MutexAutoLock lock(mVideo.mMutex);
     mVideo.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
     if (!mVideo.mTrackDemuxer) {
       mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       return;
     }
 
     UniquePtr<TrackInfo> videoInfo = mVideo.mTrackDemuxer->GetInfo();
     videoActive = videoInfo && videoInfo->IsValid();
     if (videoActive) {
       if (platform &&
           !platform->SupportsMimeType(videoInfo->mMimeType, nullptr)) {
         // We have no decoder for this track. Error.
         mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
         return;
       }
-      {
-        MutexAutoLock lock(mVideo.mMutex);
-        mInfo.mVideo = *videoInfo->GetAsVideoInfo();
-      }
+      mInfo.mVideo = *videoInfo->GetAsVideoInfo();
+      mVideo.mWorkingInfo = MakeUnique<VideoInfo>(mInfo.mVideo);
       for (const MetadataTag& tag : videoInfo->mTags) {
         tags->Put(tag.mKey, tag.mValue);
       }
       mVideo.mOriginalInfo = std::move(videoInfo);
       mTrackDemuxersMayBlock |= mVideo.mTrackDemuxer->GetSamplesMayBlock();
     } else {
       mVideo.mTrackDemuxer->BreakCycles();
       mVideo.mTrackDemuxer = nullptr;
     }
   }
 
   bool audioActive = !!mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
   if (audioActive) {
+    MutexAutoLock lock(mAudio.mMutex);
     mAudio.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
     if (!mAudio.mTrackDemuxer) {
       mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       return;
     }
 
     UniquePtr<TrackInfo> audioInfo = mAudio.mTrackDemuxer->GetInfo();
     // We actively ignore audio tracks that we know we can't play.
     audioActive =
       audioInfo && audioInfo->IsValid() &&
       (!platform || platform->SupportsMimeType(audioInfo->mMimeType, nullptr));
 
     if (audioActive) {
-      {
-        MutexAutoLock lock(mAudio.mMutex);
-        mInfo.mAudio = *audioInfo->GetAsAudioInfo();
-      }
+      mInfo.mAudio = *audioInfo->GetAsAudioInfo();
+      mAudio.mWorkingInfo = MakeUnique<AudioInfo>(mInfo.mAudio);
       for (const MetadataTag& tag : audioInfo->mTags) {
         tags->Put(tag.mKey, tag.mValue);
       }
       mAudio.mOriginalInfo = std::move(audioInfo);
       mTrackDemuxersMayBlock |= mAudio.mTrackDemuxer->GetSamplesMayBlock();
     } else {
       mAudio.mTrackDemuxer->BreakCycles();
       mAudio.mTrackDemuxer = nullptr;
@@ -1543,17 +1547,29 @@ MediaFormatReader::OnDemuxerInitFailed(c
 {
   mDemuxerInitRequest.Complete();
   mMetadataPromise.Reject(aError, __func__);
 }
 
 void
 MediaFormatReader::ReadUpdatedMetadata(MediaInfo* aInfo)
 {
-  *aInfo = mInfo;
+  // Called on the MDSM's TaskQueue.
+  {
+    MutexAutoLock lock(mVideo.mMutex);
+    if (HasVideo()) {
+      aInfo->mVideo = *mVideo.GetWorkingInfo()->GetAsVideoInfo();
+    }
+  }
+  {
+    MutexAutoLock lock(mAudio.mMutex);
+    if (HasAudio()) {
+      aInfo->mAudio = *mAudio.GetWorkingInfo()->GetAsAudioInfo();
+    }
+  }
 }
 
 MediaFormatReader::DecoderData&
 MediaFormatReader::GetDecoderData(TrackType aTrack)
 {
   MOZ_ASSERT(aTrack == TrackInfo::kAudioTrack ||
              aTrack == TrackInfo::kVideoTrack);
   if (aTrack == TrackInfo::kAudioTrack) {
@@ -2223,16 +2239,24 @@ MediaFormatReader::HandleDemuxedSamples(
     LOG("%s stream id has changed from:%d to:%d.",
         TrackTypeToStr(aTrack),
         decoder.mLastStreamSourceID,
         info->GetID());
 
     decoder.mNextStreamSourceID.reset();
     decoder.mLastStreamSourceID = info->GetID();
     decoder.mInfo = info;
+    {
+      MutexAutoLock lock(decoder.mMutex);
+      if (aTrack == TrackInfo::kAudioTrack) {
+        decoder.mWorkingInfo = MakeUnique<AudioInfo>(*info->GetAsAudioInfo());
+      } else if (aTrack == TrackInfo::kVideoTrack) {
+        decoder.mWorkingInfo = MakeUnique<VideoInfo>(*info->GetAsVideoInfo());
+      }
+    }
 
     decoder.mMeanRate.Reset();
 
     if (sample->mKeyframe) {
       if (samples.Length()) {
         decoder.mQueuedSamples = std::move(samples);
       }
     } else {
@@ -2670,26 +2694,31 @@ MediaFormatReader::ReturnOutput(MediaDat
     if (audioData->mChannels != mInfo.mAudio.mChannels ||
         audioData->mRate != mInfo.mAudio.mRate) {
       LOG("change of audio format (rate:%d->%d). "
           "This is an unsupported configuration",
           mInfo.mAudio.mRate,
           audioData->mRate);
       mInfo.mAudio.mRate = audioData->mRate;
       mInfo.mAudio.mChannels = audioData->mChannels;
+      MutexAutoLock lock(mAudio.mMutex);
+      mAudio.mWorkingInfo->GetAsAudioInfo()->mRate = audioData->mRate;
+      mAudio.mWorkingInfo->GetAsAudioInfo()->mChannels = audioData->mChannels;
     }
     mAudio.ResolvePromise(audioData, __func__);
   } else if (aTrack == TrackInfo::kVideoTrack) {
     VideoData* videoData = static_cast<VideoData*>(aData);
 
     if (videoData->mDisplay != mInfo.mVideo.mDisplay) {
       LOG("change of video display size (%dx%d->%dx%d)",
           mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height,
           videoData->mDisplay.width, videoData->mDisplay.height);
       mInfo.mVideo.mDisplay = videoData->mDisplay;
+      MutexAutoLock lock(mVideo.mMutex);
+      mVideo.mWorkingInfo->GetAsVideoInfo()->mDisplay = videoData->mDisplay;
     }
 
     TimeUnit nextKeyframe;
     if (!mVideo.HasInternalSeekPending() &&
         NS_SUCCEEDED(
           mVideo.mTrackDemuxer->GetNextRandomAccessPoint(&nextKeyframe))) {
       videoData->SetNextKeyFrameTime(nextKeyframe);
     }
@@ -3297,36 +3326,36 @@ void
 MediaFormatReader::GetMozDebugReaderData(nsACString& aString)
 {
   nsCString result;
   nsAutoCString audioDecoderName("unavailable");
   nsAutoCString videoDecoderName = audioDecoderName;
   nsAutoCString audioType("none");
   nsAutoCString videoType("none");
 
-  AudioInfo audioInfo = mAudio.GetCurrentInfo()
-                          ? *mAudio.GetCurrentInfo()->GetAsAudioInfo()
-                          : AudioInfo();
-  if (HasAudio())
+  AudioInfo audioInfo;
   {
     MutexAutoLock lock(mAudio.mMutex);
-    audioDecoderName = mAudio.mDecoder
-                       ? mAudio.mDecoder->GetDescriptionName()
-                       : mAudio.mDescription;
-    audioType = audioInfo.mMimeType;
+    if (HasAudio()) {
+      audioInfo = *mAudio.GetWorkingInfo()->GetAsAudioInfo();
+      audioDecoderName = mAudio.mDecoder ? mAudio.mDecoder->GetDescriptionName()
+                                         : mAudio.mDescription;
+      audioType = audioInfo.mMimeType;
+    }
   }
-  VideoInfo videoInfo = mVideo.GetCurrentInfo()
-                          ? *mVideo.GetCurrentInfo()->GetAsVideoInfo()
-                          : VideoInfo();
-  if (HasVideo()) {
-    MutexAutoLock mon(mVideo.mMutex);
-    videoDecoderName = mVideo.mDecoder
-                       ? mVideo.mDecoder->GetDescriptionName()
-                       : mVideo.mDescription;
-    videoType = videoInfo.mMimeType;
+
+  VideoInfo videoInfo;
+  {
+    MutexAutoLock lock(mVideo.mMutex);
+    if (HasVideo()) {
+      videoInfo = *mVideo.GetWorkingInfo()->GetAsVideoInfo();
+      videoDecoderName = mVideo.mDecoder ? mVideo.mDecoder->GetDescriptionName()
+                                         : mVideo.mDescription;
+      videoType = videoInfo.mMimeType;
+    }
   }
 
   result +=
     nsPrintfCString("Audio Decoder(%s, %u channels @ %0.1fkHz): %s\n",
                     audioType.get(),
                     audioInfo.mChannels,
                     audioInfo.mRate / 1000.0f,
                     audioDecoderName.get());
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -400,17 +400,20 @@ private:
     MediaFormatReader* mOwner;
     // Disambiguate Audio vs Video.
     MediaData::Type mType;
     RefPtr<MediaTrackDemuxer> mTrackDemuxer;
     // TaskQueue on which decoder can choose to decode.
     // Only non-null up until the decoder is created.
     RefPtr<TaskQueue> mTaskQueue;
 
-    // Mutex protecting mDescription and mDecoder.
+    // Mutex protecting mDescription, mDecoder, mTrackDemuxer and mWorkingInfo
+    // as those can be read outside the TaskQueue.
+    // They are only written on the TaskQueue however, as such mMutex doesn't
+    // need to be held when those members are read on the TaskQueue.
     Mutex mMutex;
     // The platform decoder.
     RefPtr<MediaDataDecoder> mDecoder;
     nsCString mDescription;
     void ShutdownDecoder();
 
     // Only accessed from reader's task queue.
     bool mUpdateScheduled;
@@ -583,16 +586,23 @@ private:
     // with MSE or the WebMDemuxer.
     const TrackInfo* GetCurrentInfo() const
     {
       if (mInfo) {
         return *mInfo;
       }
       return mOriginalInfo.get();
     }
+    // Return the current TrackInfo updated as per the decoder output.
+    // Typically for audio, the number of channels and/or sampling rate can vary
+    // between what was found in the metadata and what the decoder returned.
+    const TrackInfo* GetWorkingInfo() const
+    {
+      return mWorkingInfo.get();
+    }
     bool IsEncrypted() const
     {
       return GetCurrentInfo()->mCrypto.mValid;
     }
 
     // Used by the MDSM for logging purposes.
     Atomic<size_t> mSizeOfQueue;
     // Used by the MDSM to determine if video decoding is hardware accelerated.
@@ -600,16 +610,19 @@ private:
     Atomic<bool> mIsHardwareAccelerated;
     // Sample format monitoring.
     uint32_t mLastStreamSourceID;
     Maybe<uint32_t> mNextStreamSourceID;
     media::TimeIntervals mTimeRanges;
     Maybe<media::TimeUnit> mLastTimeRangesEnd;
     // TrackInfo as first discovered during ReadMetadata.
     UniquePtr<TrackInfo> mOriginalInfo;
+    // Written exclusively on the TaskQueue, can be read on MDSM's TaskQueue.
+    // Must be read with parent's mutex held.
+    UniquePtr<TrackInfo> mWorkingInfo;
     RefPtr<TrackInfoSharedPtr> mInfo;
     Maybe<media::TimeUnit> mFirstDemuxedSampleTime;
     // Use NullDecoderModule or not.
     bool mIsNullDecode;
 
     class
     {
     public:
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
@@ -1,24 +1,25 @@
 package org.mozilla.gecko.prompts;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
+import org.mozilla.gecko.util.BitmapUtils;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.VectorDrawable;
-import android.support.v4.content.ContextCompat;
+import android.os.Build;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.TextViewCompat;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CheckedTextView;
 import android.widget.TextView;
 import android.widget.ListView;
@@ -113,21 +114,21 @@ public class PromptListAdapter extends A
 
         Drawable d = null;
         Resources res = getContext().getResources();
         // Set the padding between the icon and the text.
         t.setCompoundDrawablePadding(mIconTextPadding);
         if (icon != null) {
             // We want the icon to be of a specific size. Some do not
             // follow this rule so we have to resize them.
-            if (icon instanceof BitmapDrawable) {
-                Bitmap bitmap = ((BitmapDrawable) icon).getBitmap();
+            if (icon instanceof BitmapDrawable ||
+                    (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon instanceof AdaptiveIconDrawable)) {
+                Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(icon);
                 d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
             } else {
-                // FIXME: Fix scale issue for AdaptiveIconDrawable in bug 1397174
                 d = icon;
             }
 
         } else if (item.inGroup) {
             // We don't currently support "indenting" items with icons
             d = getBlankDrawable(res);
         }
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5418,16 +5418,18 @@ pref("network.trr.bootstrapAddress", "")
 // Meant to survive basically a page load.
 pref("network.trr.blacklist-duration", 60);
 // Single TRR request timeout, in milliseconds
 pref("network.trr.request-timeout", 1500);
 // Allow AAAA entries to be used "early", before the A results are in
 pref("network.trr.early-AAAA", false);
 // Explicitly disable ECS (EDNS Client Subnet, RFC 7871)
 pref("network.trr.disable-ECS", true);
+// After this many failed TRR requests in a row, consider TRR borked
+pref("network.trr.max-fails", 5);
 
 pref("captivedetect.canonicalURL", "http://detectportal.firefox.com/success.txt");
 pref("captivedetect.canonicalContent", "success\n");
 pref("captivedetect.maxWaitingTime", 5000);
 pref("captivedetect.pollingTime", 3000);
 pref("captivedetect.maxRetryCount", 5);
 
 #ifdef RELEASE_OR_BETA
--- a/netwerk/dns/TRR.cpp
+++ b/netwerk/dns/TRR.cpp
@@ -993,16 +993,19 @@ TRR::OnStopRequest(nsIRequest *aRequest,
                    nsresult aStatusCode)
 {
   // The dtor will be run after the function returns
   LOG(("TRR:OnStopRequest %p %s %d failed=%d code=%X\n",
        this, mHost.get(), mType, mFailed, (unsigned int)aStatusCode));
   nsCOMPtr<nsIChannel> channel;
   channel.swap(mChannel);
 
+  // Bad content is still considered "okay" if the HTTP response is okay
+  gTRRService->TRRIsOkay(NS_SUCCEEDED(aStatusCode));
+
   // if status was "fine", parse the response and pass on the answer
   if (!mFailed && NS_SUCCEEDED(aStatusCode)) {
     nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
     if (!httpChannel) {
       return NS_ERROR_UNEXPECTED;
     }
     nsresult rv = NS_OK;
     nsAutoCString contentType;
@@ -1129,16 +1132,17 @@ TRR::Cancel()
   if (!NS_IsMainThread()) {
     NS_DispatchToMainThread(new ProxyCancel(this));
     return;
   }
   if (mChannel) {
     LOG(("TRR: %p canceling Channel %p %s %d\n", this,
          mChannel.get(), mHost.get(), mType));
     mChannel->Cancel(NS_ERROR_ABORT);
+    gTRRService->TRRIsOkay(false);
   }
 }
 
 #undef LOG
 
 // namespace
 }
 }
--- a/netwerk/dns/TRRService.cpp
+++ b/netwerk/dns/TRRService.cpp
@@ -40,19 +40,21 @@ TRRService::TRRService()
   , mTRRTimeout(3000)
   , mLock("trrservice")
   , mConfirmationNS(NS_LITERAL_CSTRING("example.com"))
   , mWaitForCaptive(true)
   , mRfc1918(false)
   , mCaptiveIsPassed(false)
   , mUseGET(false)
   , mDisableECS(true)
+  , mDisableAfterFails(5)
   , mClearTRRBLStorage(false)
   , mConfirmationState(CONFIRM_INIT)
   , mRetryConfirmInterval(1000)
+  , mTRRFailures(0)
 {
   MOZ_ASSERT(NS_IsMainThread(), "wrong thread");
 }
 
 nsresult
 TRRService::Init()
 {
   MOZ_ASSERT(NS_IsMainThread(), "wrong thread");
@@ -260,16 +262,22 @@ TRRService::ReadPrefs(const char *name)
     }
   }
   if (!name || !strcmp(name, TRR_PREF("disable-ECS"))) {
     bool tmp;
     if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("disable-ECS"), &tmp))) {
       mDisableECS = tmp;
     }
   }
+  if (!name || !strcmp(name, TRR_PREF("max-fails"))) {
+    uint32_t fails;
+    if (NS_SUCCEEDED(Preferences::GetUint(TRR_PREF("max-fails"), &fails))) {
+      mDisableAfterFails = fails;
+    }
+  }
 
   return NS_OK;
 }
 
 nsresult
 TRRService::GetURI(nsCString &result)
 {
   MutexAutoLock lock(mLock);
@@ -584,16 +592,36 @@ TRRService::Notify(nsITimer *aTimer)
   } else {
     MOZ_CRASH("Unknown timer");
   }
 
   return NS_OK;
 }
 
 
+void
+TRRService::TRRIsOkay(bool aWorks)
+{
+  if (aWorks) {
+    mTRRFailures = 0;
+  } else if ((mMode == MODE_TRRFIRST) && (mConfirmationState == CONFIRM_OK)) {
+    // only count failures while in OK state
+    uint32_t fails = ++mTRRFailures;
+    if (fails >= mDisableAfterFails) {
+      LOG(("TRRService goes FAILED after %u failures in a row\n", fails));
+      mConfirmationState = CONFIRM_FAILED;
+      // Fire off a timer and start re-trying the NS domain again
+      NS_NewTimerWithCallback(getter_AddRefs(mRetryConfirmTimer),
+                              this, mRetryConfirmInterval,
+                              nsITimer::TYPE_ONE_SHOT);
+      mTRRFailures = 0; // clear it again
+    }
+  }
+}
+
 AHostResolver::LookupStatus
 TRRService::CompleteLookup(nsHostRecord *rec, nsresult status, AddrInfo *aNewRRSet, bool pb)
 {
   // this is an NS check for the TRR blacklist or confirmationNS check
 
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!rec);
 
@@ -602,18 +630,18 @@ TRRService::CompleteLookup(nsHostRecord 
 
   MOZ_ASSERT(!mConfirmer || (mConfirmationState == CONFIRM_TRYING));
   if (mConfirmationState == CONFIRM_TRYING) {
     MOZ_ASSERT(mConfirmer);
     mConfirmationState = NS_SUCCEEDED(status) ? CONFIRM_OK : CONFIRM_FAILED;
     LOG(("TRRService finishing confirmation test %s %d %X\n",
          mPrivateURI.get(), (int)mConfirmationState, (unsigned int)status));
     mConfirmer = nullptr;
-    if ((mConfirmationState == CONFIRM_FAILED) && (mMode == MODE_TRRONLY)) {
-      // in TRR-only mode; retry failed confirmations
+    if (mConfirmationState == CONFIRM_FAILED) {
+      // retry failed NS confirmation
       NS_NewTimerWithCallback(getter_AddRefs(mRetryConfirmTimer),
                               this, mRetryConfirmInterval,
                               nsITimer::TYPE_ONE_SHOT);
       if (mRetryConfirmInterval < 64000) {
         // double the interval up to this point
         mRetryConfirmInterval *= 2;
       }
     } else {
--- a/netwerk/dns/TRRService.h
+++ b/netwerk/dns/TRRService.h
@@ -44,16 +44,17 @@ public:
   uint32_t GetRequestTimeout() { return mTRRTimeout; }
 
   LookupStatus CompleteLookup(nsHostRecord *, nsresult, mozilla::net::AddrInfo *, bool pb) override;
   LookupStatus CompleteLookupByType(nsHostRecord *, nsresult, const nsTArray<nsCString> *, uint32_t, bool pb) override;
   void TRRBlacklist(const nsACString &host, bool privateBrowsing, bool aParentsToo);
   bool IsTRRBlacklisted(const nsACString &host, bool privateBrowsing, bool fullhost);
 
   bool MaybeBootstrap(const nsACString &possible, nsACString &result);
+  void TRRIsOkay(bool aWorks);
 
 private:
   virtual  ~TRRService();
   nsresult ReadPrefs(const char *name);
   void GetPrefBranch(nsIPrefBranch **result);
   void MaybeConfirm();
 
   bool                      mInitialized;
@@ -69,31 +70,33 @@ private:
 
   Atomic<bool, Relaxed> mWaitForCaptive; // wait for the captive portal to say OK before using TRR
   Atomic<bool, Relaxed> mRfc1918; // okay with local IP addresses in DOH responses?
   Atomic<bool, Relaxed> mCaptiveIsPassed; // set when captive portal check is passed
   Atomic<bool, Relaxed> mUseGET; // do DOH using GET requests (instead of POST)
   Atomic<bool, Relaxed> mEarlyAAAA; // allow use of AAAA results before A is in
   Atomic<bool, Relaxed> mDisableIPv6; // don't even try
   Atomic<bool, Relaxed> mDisableECS;  // disable EDNS Client Subnet in requests
+  Atomic<uint32_t, Relaxed> mDisableAfterFails;  // this many fails in a row means failed TRR service
 
   // TRR Blacklist storage
   RefPtr<DataStorage> mTRRBLStorage;
   Atomic<bool, Relaxed> mClearTRRBLStorage;
 
   enum ConfirmationState {
     CONFIRM_INIT = 0,
     CONFIRM_TRYING = 1,
     CONFIRM_OK = 2,
     CONFIRM_FAILED = 3
   };
   Atomic<ConfirmationState, Relaxed>  mConfirmationState;
   RefPtr<TRR> mConfirmer;
   nsCOMPtr<nsITimer> mRetryConfirmTimer;
   uint32_t mRetryConfirmInterval; // milliseconds until retry
+  Atomic<uint32_t, Relaxed> mTRRFailures;
 };
 
 extern TRRService *gTRRService;
 
 } // namespace net
 } // namespace mozilla
 
 #endif // TRRService_h_
--- 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,16 +1,4 @@
 [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
-
-  [<rp> in a <select> ("<div><select class='poke-rp'></select>")]
-    expected: FAIL
-