Bug 1543377 - Add the abuse reporting WebComponents. r=mstriemer,flod,mixedpuppy,robwu
authorLuca Greco <lgreco@mozilla.com>
Mon, 06 May 2019 18:45:01 +0000
changeset 531565 274e9115edf07409a34b56f128ac5e117b53d877
parent 531564 e70459131e5a6186b3e08995b42a08c754a2229e
child 531566 d15c62c6826bf796cabf60ea8ea23988aedea17a
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstriemer, flod, mixedpuppy, robwu
bugs1543377
milestone68.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
Bug 1543377 - Add the abuse reporting WebComponents. r=mstriemer,flod,mixedpuppy,robwu Depends on D27938 and D27547 Differential Revision: https://phabricator.services.mozilla.com/D26906
modules/libpref/init/all.js
toolkit/locales/en-US/toolkit/about/abuseReports.ftl
toolkit/mozapps/extensions/content/aboutaddons.html
toolkit/mozapps/extensions/content/abuse-report-frame.html
toolkit/mozapps/extensions/content/abuse-report-frame.js
toolkit/mozapps/extensions/content/abuse-report-panel.css
toolkit/mozapps/extensions/content/abuse-report-panel.js
toolkit/mozapps/extensions/content/abuse-reports.js
toolkit/mozapps/extensions/content/extensions.xul
toolkit/mozapps/extensions/jar.mn
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2725,16 +2725,17 @@ pref("services.settings.default_signer",
 pref("services.common.uptake.sampleRate", 1);   // 1%
 
 // Security state OneCRL.
 pref("services.settings.security.onecrl.bucket", "security-state");
 pref("services.settings.security.onecrl.collection", "onecrl");
 pref("services.settings.security.onecrl.signer", "onecrl.content-signature.mozilla.org");
 pref("services.settings.security.onecrl.checked", 0);
 
+pref("extensions.abuseReport.enabled", false);
 pref("extensions.abuseReport.url", "https://addons.mozilla.org/api/v4/abuse/report/addon/");
 
 // Blocklist preferences
 pref("extensions.blocklist.enabled", true);
 // OneCRL freshness checking depends on this value, so if you change it,
 // please also update security.onecrl.maximum_staleness_in_seconds.
 pref("extensions.blocklist.interval", 86400);
 // Required blocklist freshness for OneCRL OCSP bypass
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/toolkit/about/abuseReports.ftl
@@ -0,0 +1,89 @@
+# 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/.
+
+abuse-report-title-extension = Report This Extension to { -vendor-short-name }
+abuse-report-title-theme = Report This Theme to { -vendor-short-name }
+abuse-report-subtitle = What’s the issue?
+
+# Variables:
+#   $author-name (string) - Name of the add-on author
+abuse-report-addon-authored-by = by <a data-l10n-name="author-name">{ $author-name }</a>
+
+abuse-report-learnmore =
+  Unsure what issue to select?
+  <a data-l10n-name="learnmore-link">Learn more about reporting extensions and themes</a>
+
+abuse-report-submit-description = Describe the problem (optional)
+abuse-report-textarea =
+  .placeholder = It’s easier for us to address a problem if we have specifics. Please describe what you’re experiencing. Thank you for helping us keep the web healthy.
+abuse-report-submit-note =
+  Note: Don’t include personal information (such as name, email address, phone number, physical address).
+  { -vendor-short-name } keeps a permanent record of these reports.
+
+## Panel buttons.
+
+abuse-report-cancel-button = Cancel
+abuse-report-next-button = Next
+abuse-report-goback-button = Go back
+abuse-report-submit-button = Submit
+
+## Message bars descriptions.
+
+## Variables:
+##   $addon-name (string) - Name of the add-on
+abuse-report-messagebar-aborted = Report for <span data-l10n-name="addon-name">{ $addon-name }</span> canceled.
+abuse-report-messagebar-submitting = Sending report for <span data-l10n-name="addon-name">{ $addon-name }</span>.
+abuse-report-messagebar-submitted = Thank you for submitting a report. Do you want to remove <span data-l10n-name="addon-name">{ $addon-name }</span>?
+abuse-report-messagebar-removed-extension = Thank you for submitting a report. You’ve removed the extension <span data-l10n-name="addon-name">{ $addon-name }</span>.
+abuse-report-messagebar-removed-theme = Thank you for submitting a report. You’ve removed the theme <span data-l10n-name="addon-name">{ $addon-name }</span>.
+abuse-report-messagebar-error = There was an error sending the report for <span data-l10n-name="addon-name">{ $addon-name }</span>.
+abuse-report-messagebar-error-recent-submit = The report for <span data-l10n-name="addon-name">{ $addon-name }</span> wasn’t sent because another report was submitted recently.
+
+## Message bars actions.
+
+abuse-report-messagebar-action-remove = Yes, Remove It
+abuse-report-messagebar-action-keep = No, I’ll Keep It
+abuse-report-messagebar-action-retry = Retry
+abuse-report-messagebar-action-cancel = Cancel
+
+## Abuse report reasons (optionally paired with related examples and/or suggestions)
+
+abuse-report-damage-reason = Damages my computer and data
+abuse-report-damage-example = Example: Injected malware or stole data
+
+abuse-report-spam-reason = Creates spam or advertising
+abuse-report-spam-example = Example: Insert ads on webpages
+
+abuse-report-settings-reason = Changed my search engine, homepage, or new tab without informing or asking me
+abuse-report-settings-suggestions = Before reporting the extension, you can try changing your settings:
+abuse-report-settings-suggestions-search = Change your default search settings
+abuse-report-settings-suggestions-homepage = Change your homepage and new tab
+
+abuse-report-deceptive-reason = Pretend to be something it’s not
+abuse-report-deceptive-example = Example: Misleading description or imagery
+
+abuse-report-broken-reason-extension = Doesn’t work, breaks websites, or slows { -brand-product-name } down
+abuse-report-broken-reason-theme = Doesn’t work or breaks browser display
+abuse-report-broken-example =
+  Example: Feature are slow, hard to use, or don’t work; parts of websites won’t load or look unusual
+abuse-report-broken-suggestions-extension =
+  It sounds like you’ve identified a bug. In addition to submitting a report here, the best way
+  to get a functionality issue resolved is to contact the extension developer.
+  <a data-l10n-name="support-link">Visit the extension’s website</a> to get the developer information.
+abuse-report-broken-suggestions-theme =
+  It sounds like you’ve identified a bug. In addition to submitting a report here, the best way
+  to get a functionality issue resolved is to contact the theme developer.
+  <a data-l10n-name="support-link">Visit the theme’s website</a> to get the developer information.
+
+abuse-report-policy-reason = Hateful, violent, or illegal content
+abuse-report-policy-suggestions =
+  Note: Copyright and trademark issues must be reported in a separate process.
+  <a data-l10n-name="report-infringement-link">Use these instructions</a> to
+  report the problem.
+
+abuse-report-unwanted-reason = Never wanted this extension and can’t get rid of it
+abuse-report-unwanted-example = Example: An application installed it without my permission
+
+abuse-report-other-reason = Something else
+
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -1,19 +1,21 @@
 <!DOCTYPE html>
 <html>
   <head>
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" href="chrome://mozapps/content/extensions/aboutaddons.css">
 
     <link rel="localization" href="branding/brand.ftl">
     <link rel="localization" href="toolkit/about/aboutAddons.ftl">
+    <link rel="localization" href="toolkit/about/abuseReports.ftl">
 
     <script src="chrome://mozapps/content/extensions/aboutaddonsCommon.js"></script>
     <script src="chrome://mozapps/content/extensions/message-bar.js"></script>
+    <script src="chrome://mozapps/content/extensions/abuse-reports.js"></script>
     <script src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
   </head>
   <body>
     <message-bar-stack id="abuse-reports-messages" reverse max-message-bar-count="3">
     </message-bar-stack>
     <div id="main">
     </div>
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-report-frame.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
+    <link rel="stylesheet" href="chrome://mozapps/content/extensions/aboutaddons.css">
+    <link rel="stylesheet" href="chrome://mozapps/content/extensions/abuse-report-panel.css">
+
+    <link rel="localization" href="branding/brand.ftl">
+    <link rel="localization" href="toolkit/about/aboutAddons.ftl">
+    <link rel="localization" href="toolkit/about/abuseReports.ftl">
+
+    <script src="chrome://mozapps/content/extensions/abuse-report-panel.js"></script>
+  </head>
+
+  <body tabindex="0">
+    <!-- WebComponents Templates -->
+    <template id="tmpl-abuse-report">
+      <div class="modal-overlay-outer"></div>
+      <div class="modal-panel-container">
+        <form class="card addon-abuse-report" onsubmit="return false;">
+          <div class="abuse-report-header">
+            <img class="card-heading-icon addon-icon"/>
+            <div class="card-contents">
+              <span class="addon-name"></span>
+              <span class="addon-author-box"
+                    data-l10n-args='{"author-name": "author placeholder"}'
+                    data-l10n-id="abuse-report-addon-authored-by">
+                <a data-l10n-name="author-name"
+                   class="author" href="#" target="_blank"></a>
+              </span>
+            </div>
+          </div>
+          <button class="abuse-report-close-icon"></button>
+          <div class="abuse-report-contents">
+            <abuse-report-reasons-panel></abuse-report-reasons-panel>
+            <abuse-report-submit-panel hidden></abuse-report-submit-panel>
+          </div>
+          <div class="abuse-report-buttons">
+            <div class="abuse-report-reasons-buttons">
+              <button class="abuse-report-cancel"
+                      data-l10n-id="abuse-report-cancel-button">
+              </button>
+              <button class="primary abuse-report-next"
+                      data-l10n-id="abuse-report-next-button">
+              </button>
+            </div>
+            <div class="abuse-report-submit-buttons" hidden>
+              <button class="abuse-report-goback"
+                      data-l10n-id="abuse-report-goback-button">
+              </button>
+              <button class="primary abuse-report-submit"
+                      data-l10n-id="abuse-report-submit-button">
+              </button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </template>
+
+    <template id="tmpl-reasons-panel">
+      <h2 class="abuse-report-title"></h2>
+      <hr>
+      <p class="abuse-report-subtitle"
+         data-l10n-id="abuse-report-subtitle">
+      </p>
+      <ul class="abuse-report-reasons">
+        <li is="abuse-report-reason-listitem" report-reason="other"></li>
+      </ul>
+      <p data-l10n-id="abuse-report-learnmore">
+        <a class="abuse-report-learnmore" target="_blank"
+           data-l10n-name="learnmore-link">
+        </a>
+      </p>
+    </template>
+
+    <template id="tmpl-submit-panel">
+      <h2 class="abuse-reason-title"></h2>
+      <abuse-report-reason-suggestions></abuse-report-reason-suggestions>
+      <hr>
+      <p class="abuse-report-subtitle" data-l10n-id="abuse-report-submit-description">
+      </p>
+      <textarea name="message" data-l10n-id="abuse-report-textarea"></textarea>
+      <p class="abuse-report-note" data-l10n-id="abuse-report-submit-note">
+      </p>
+    </template>
+
+    <template id="tmpl-reason-listitem">
+      <label>
+        <input type="radio" name="reason" class="radio">
+        <span class="reason-description"></span>
+        <span hidden class="abuse-report-note reason-example"></span>
+      </label>
+    </template>
+
+    <template id="tmpl-suggestions-settings">
+      <p data-l10n-id="abuse-report-settings-suggestions"><p>
+      <ul>
+        <li>
+          <a class="abuse-settings-search-learnmore" target="_blank"
+             data-l10n-id="abuse-report-settings-suggestions-search">
+          </a>
+        </li>
+        <li>
+          <a class="abuse-settings-homepage-learnmore" target="_blank"
+             data-l10n-id="abuse-report-settings-suggestions-homepage">
+          </a>
+        </li>
+      </ul>
+    </template>
+
+    <template id="tmpl-suggestions-policy">
+      <p data-l10n-id="abuse-report-policy-suggestions">
+        <a class="abuse-policy-learnmore" target="_blank"
+           data-l10n-name="report-infringement-link">
+        </a>
+      </p>
+    </template>
+
+    <template id="tmpl-suggestions-broken-extension">
+      <p data-l10n-id="abuse-report-broken-suggestions-extension">
+        <a class="extension-support-link" target="_blank"
+           data-l10n-name="support-link">
+        </a>
+      <p>
+    </template>
+
+    <template id="tmpl-suggestions-broken-theme">
+      <p data-l10n-id="abuse-report-broken-suggestions-theme">
+        <a class="extension-support-link" target="_blank"
+           data-l10n-name="support-link">
+        </a>
+      <p>
+    </template>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-report-frame.js
@@ -0,0 +1,232 @@
+"use strict";
+
+/* globals MozXULElement, Services, useHtmlViews, getHtmlBrowser, htmlBrowserLoaded */
+
+{
+  const ABUSE_REPORT_ENABLED = Services.prefs.getBoolPref("extensions.abuseReport.enabled", false);
+  const ABUSE_REPORT_FRAME_URL = "chrome://mozapps/content/extensions/abuse-report-frame.html";
+  const fm = Services.focus;
+  const {AbuseReporter} = ChromeUtils.import("resource://gre/modules/AbuseReporter.jsm");
+
+  class AddonAbuseReportsXULFrame extends MozXULElement {
+    constructor() {
+      super();
+      this.report = null;
+      // Keep track if the loadURI has already been called on the
+      // browser element.
+      this.browserLoadURI = false;
+    }
+
+    connectedCallback() {
+      this.textContent = "";
+
+      const content = MozXULElement.parseXULToFragment(`
+        <browser id="abuse-report-xulframe-overlay-inner"
+          type="content"
+          disablehistory="true"
+          transparent="true"
+          flex="1">
+        </browser>
+      `);
+
+      this.appendChild(content);
+
+      const browser = this.querySelector("browser");
+      this.promiseBrowserLoaded = new Promise(resolve => {
+        browser.addEventListener("load", () => resolve(browser), {once: true});
+      });
+
+      document.addEventListener("focus", this);
+
+      this.update();
+    }
+
+    disconnectedCallback() {
+      this.textContent = "";
+      this.browserLoadURI = false;
+      this.promiseBrowserLoaded = null;
+      this.report = null;
+      document.removeEventListener("focus", this);
+    }
+
+    handleEvent(evt) {
+      // "abuse-report:cancel", "abuse-report:submit" and "abuse-report:updated" are
+      // all dispatched from the AbuseReport webcomponent (on the AbuseReport element itself).
+      // All the "abuse-report:*" events are also forwarded (dispatched on the frame
+      // DOM element itself) to make it easier for the tests to wait for certain conditions
+      // to be reached.
+      switch (evt.type) {
+        case "focus":
+          this.focus();
+          break;
+        case "abuse-report:cancel":
+          this.cancelReport();
+          this.forwardEvent(evt);
+          break;
+        case "abuse-report:submit":
+          this.onSubmitReport(evt);
+          this.forwardEvent(evt);
+          break;
+        case "abuse-report:updated":
+          this.forwardEvent(evt);
+          break;
+      }
+    }
+
+    forwardEvent(evt) {
+      this.dispatchEvent(new CustomEvent(evt.type, {detail: evt.detail}));
+    }
+
+    async openReport({addonId, reportEntryPoint}) {
+      if (this.report) {
+        throw new Error("Ignoring new abuse report request. AbuseReport panel already open");
+      } else {
+        try {
+          this.report = await AbuseReporter.createAbuseReport(addonId, {reportEntryPoint});
+          this.update();
+        } catch (err) {
+          // Log the complete error in the console.
+          console.error("Error creating abuse report for", addonId, err);
+          // The report has an error on creation, and so instead of opening the report
+          // panel an error message-bar is created on the HTML about:addons page.
+          const win = await this.promiseHtmlAboutAddons;
+          win.document.dispatchEvent(
+            new CustomEvent("abuse-report:create-error", {detail: {
+              addonId, addon: err.addon, errorType: err.errorType,
+            }}));
+        }
+      }
+    }
+
+    cancelReport() {
+      if (this.report) {
+        this.report.abort();
+        this.report = null;
+        this.update();
+      }
+    }
+
+    async onSubmitReport(evt) {
+      if (this.report) {
+        this.report = null;
+        this.update();
+        const win = await this.promiseHtmlAboutAddons;
+        win.document.dispatchEvent(evt);
+      }
+    }
+
+    focus() {
+      // Trap the focus in the abuse report modal while it is enabled.
+      if (this.hasAddonId) {
+        this.promiseAbuseReport.then(abuseReport => {
+          abuseReport.focus();
+        });
+      }
+    }
+
+    async update() {
+      const {report} = this;
+      if (report && report.addon && !report.errorType) {
+        const {addon, reportEntryPoint} = this.report;
+        this.addonId = addon.id;
+        this.reportEntryPoint = reportEntryPoint;
+
+        // Set the addon id on the addon-abuse-report webcomponent instance
+        // embedded in the XUL browser.
+        this.promiseAbuseReport.then(abuseReport => {
+          abuseReport.addEventListener("abuse-report:updated", this, {once: true});
+          abuseReport.addEventListener("abuse-report:submit", this, {once: true});
+          abuseReport.addEventListener("abuse-report:cancel", this, {once: true});
+          abuseReport.setAbuseReport(report);
+          this.hidden = false;
+          // Hide the content of the underlying about:addons page from
+          // screen readers.
+          this.aboutAddonsContent.setAttribute("aria-hidden", true);
+          // Move the focus to the embedded window.
+          fm.moveFocus(abuseReport.ownerGlobal, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY);
+          this.dispatchEvent(new CustomEvent("abuse-report:frame-shown"));
+        });
+      } else {
+        this.hidden = true;
+        this.removeAttribute("addon-id");
+        this.removeAttribute("report-entry-point");
+
+        // Make the content of the underlying about:addons page visible
+        // to screen readers.
+        this.aboutAddonsContent.setAttribute("aria-hidden", false);
+
+        // Move the focus back to the top level window.
+        fm.moveFocus(window, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY);
+        this.promiseAbuseReport.then(abuseReport => {
+          abuseReport.removeEventListener("abuse-report:updated", this, {once: true});
+          abuseReport.removeEventListener("abuse-report:submit", this, {once: true});
+          abuseReport.removeEventListener("abuse-report:cancel", this, {once: true});
+          abuseReport.setAbuseReport(null);
+        }, err => {
+          console.error("promiseAbuseReport rejected", err);
+        }).then(() => {
+          this.dispatchEvent(new CustomEvent("abuse-report:frame-hidden"));
+        });
+      }
+    }
+
+    get aboutAddonsContent() {
+      return document.getElementById("main-page-content");
+    }
+
+    get promiseAbuseReport() {
+      if (!this.browserLoadURI) {
+        const browser = this.querySelector("browser");
+        browser.loadURI(ABUSE_REPORT_FRAME_URL, {
+          triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+        });
+        this.browserLoadURI = true;
+      }
+      return this.promiseBrowserLoaded.then(browser => {
+        return browser.contentDocument.querySelector("addon-abuse-report");
+      });
+    }
+
+    get promiseHtmlAboutAddons() {
+      const browser = getHtmlBrowser();
+      return htmlBrowserLoaded.then(() => {
+        return browser.contentWindow;
+      });
+    }
+
+    get hasAddonId() {
+      return !!this.addonId;
+    }
+
+    get addonId() {
+      return this.getAttribute("addon-id");
+    }
+
+    set addonId(value) {
+      this.setAttribute("addon-id", value);
+    }
+
+    get reportEntryPoint() {
+      return this.getAttribute("report-entry-point");
+    }
+
+    set reportEntryPoint(value) {
+      this.setAttribute("report-entry-point", value);
+    }
+  }
+
+  // If the html about:addons and the abuse report are both enabled, register
+  // the custom XUL WebComponent and append it to the XUL stack element
+  // (if not registered the element will be just a dummy hidden box)
+  if (useHtmlViews && ABUSE_REPORT_ENABLED) {
+    customElements.define("addon-abuse-report-xulframe", AddonAbuseReportsXULFrame);
+  }
+
+  // Helper method exported into the about:addons global, used to open the
+  // abuse report panel from outside of the about:addons page
+  // (e.g. triggered from the browserAction context menu).
+  window.openAbuseReport = ({addonId, reportEntryPoint}) => {
+    const frame = document.querySelector("addon-abuse-report-xulframe");
+    frame.openReport({addonId, reportEntryPoint});
+  };
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-report-panel.css
@@ -0,0 +1,187 @@
+/* Abuse Reports card */
+
+:root {
+  --close-icon-url: url("chrome://global/skin/icons/close.svg");
+  --close-icon-size: 20px;
+
+  --radio-image-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E %3Ccircle cx='8' cy='8' r='4' fill='%23fff'/%3E %3C/svg%3E");
+  --radio-size: 16px;
+
+  --modal-panel-min-width: 60%;
+  --modal-panel-margin-top: 36px;
+  --modal-panel-margin-bottom: 36px;
+  --modal-panel-margin: 20%;
+  --modal-panel-padding: 40px;
+
+  --line-height: 20px;
+  --textarea-height: 220px;
+  --button-padding: 52px;
+  --listitem-padding-bottom: 14px;
+  --list-radio-column-size: 28px;
+  --note-font-size: 14px;
+  --note-font-weight: 400;
+  --subtitle-font-size: 16px;
+  --subtitle-font-weight: 600;
+}
+
+/* Ensure that the document (embedded in the XUL about:addons using a
+   XUL browser) has a transparent background */
+html {
+  background-color: transparent;
+}
+
+.modal-overlay-outer {
+  background: rgba(12, 12, 13, 0.6);
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  z-index: -1;
+}
+
+.modal-panel-container {
+  padding-top: var(--modal-panel-margin-top);
+  padding-bottom: var(--modal-panel-margin-bottom);
+  padding-left: var(--modal-panel-margin);
+  padding-right: var(--modal-panel-margin);
+}
+
+.addon-abuse-report {
+  min-width: var(--modal-panel-min-width);
+  padding: var(--modal-panel-padding);
+  display: flex;
+  flex-direction: column;
+  position: relative;
+}
+
+.addon-abuse-report:hover {
+  /* Avoid the card box highlighting on hover. */
+  box-shadow: none;
+}
+
+.addon-abuse-report button {
+  padding: 0 var(--button-padding);
+}
+
+.abuse-report-close-icon {
+  /* position the close button in the panel upper-right corner */
+  position: absolute;
+  top: 12px;
+  inset-inline-end: 16px;
+}
+
+button.abuse-report-close-icon {
+  background: var(--close-icon-url) no-repeat center center;
+  -moz-context-properties: fill, fill-opacity;
+  color: inherit !important;
+  fill: currentColor;
+  fill-opacity: 0;
+  min-width: auto;
+  min-height: auto;
+  width: var(--close-icon-size);
+  height: var(--close-icon-size);
+  margin: 0;
+  padding: 0;
+}
+
+button.abuse-report-close-icon:hover {
+  fill-opacity: 0.1;
+}
+
+button.abuse-report-close-icon:hover:active {
+  fill-opacity: 0.2;
+}
+
+.abuse-report-header {
+  display: flex;
+  flex-direction: row;
+}
+
+.abuse-report-contents,
+.abuse-report-contents > hr {
+  width: 100%;
+}
+
+.abuse-report-note {
+  color: var(--grey-50);
+  font-size: var(--note-font-size);
+  font-weight: var(--note-font-weight);
+  line-height: var(--line-height);
+}
+
+.abuse-report-subtitle {
+  font-size: var(--subtitle-font-size);
+  font-weight: var(--subtitle-font-weight);
+  line-height: var(--line-height);
+}
+
+ul.abuse-report-reasons {
+  list-style-type: none;
+  padding-inline-start: 0;
+}
+
+ul.abuse-report-reasons > li {
+  display: flex;
+  padding-bottom: var(--listitem-padding-bottom);
+}
+
+ul.abuse-report-reasons > li > label {
+  width: 100%;
+  line-height: var(--line-height);
+  font-size: var(--subtitle-font-size);
+  font-weight: var(--note-font-weight);
+}
+
+ul.abuse-report-reasons > li > label {
+  display: grid;
+  grid-template-columns: var(--list-radio-column-size) auto;
+  grid-template-rows: 50% auto;
+}
+
+ul.abuse-report-reasons > li > label > [type=radio] {
+  grid-column: 1;
+}
+
+ul.abuse-report-reasons > li > label > span {
+  grid-column: 2;
+}
+
+ul.abuse-report-reasons > li > label > span:nth-child(2) {
+  padding-top: 2px;
+}
+
+.abuse-report-contents [type=radio] {
+  -moz-appearance: none;
+  height: var(--radio-size);
+  width: var(--radio-size);
+  border-radius: 100%;
+  border: 1px solid var(--grey-90-a30);
+  background-color: var(--grey-90-a10);
+  margin-inline-start: 4px;
+  margin-inline-end: 4px;
+}
+
+.abuse-report-contents [type=radio]:focus {
+  box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+.abuse-report-contents [type=radio]:hover {
+  background-color: var(--grey-90-a20);
+}
+
+.abuse-report-contents [type=radio]:checked {
+  background-image: var(--radio-image-url);
+  background-color: var(--blue-60);
+  background-position: center center;
+}
+
+.abuse-report-contents [type=radio]:checked:hover {
+  background-color: var(--blue-70);
+}
+
+abuse-report-submit-panel textarea {
+  width: 100%;
+  height: var(--textarea-height);
+  resize: none;
+  box-sizing: border-box;
+}
+
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-report-panel.js
@@ -0,0 +1,675 @@
+/* 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/. */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+
+const showOnAnyType = () => false;
+const hideOnAnyType = () => true;
+const hideOnThemeType = (addonType) => addonType === "theme";
+
+// The reasons string used as a key in this Map is expected to stay in sync
+// with the reasons string used in the "abuseReports.ftl" locale file and
+// the suggestions templates included in abuse-reports-xulframe.html.
+const ABUSE_REASONS = window.ABUSE_REPORT_REASONS = {
+  "damage": {
+    isExampleHidden: showOnAnyType,
+    isReasonHidden: hideOnThemeType,
+  },
+  "spam": {
+    isExampleHidden: showOnAnyType,
+    isReasonHidden: showOnAnyType,
+  },
+  "settings": {
+    hasSuggestions: true,
+    isExampleHidden: hideOnAnyType,
+    isReasonHidden: hideOnThemeType,
+  },
+  "deceptive": {
+    isExampleHidden: showOnAnyType,
+    isReasonHidden: showOnAnyType,
+  },
+  "broken": {
+    hasAddonTypeL10nId: true,
+    hasAddonTypeSuggestionTemplate: true,
+    hasSuggestions: true,
+    isExampleHidden: hideOnThemeType,
+    isReasonHidden: showOnAnyType,
+  },
+  "policy": {
+    hasSuggestions: true,
+    isExampleHidden: hideOnAnyType,
+    isReasonHidden: showOnAnyType,
+  },
+  "unwanted": {
+    isExampleHidden: showOnAnyType,
+    isReasonHidden: hideOnThemeType,
+  },
+  "other": {
+    isExampleHidden: hideOnAnyType,
+    isReasonHidden: showOnAnyType,
+  },
+};
+
+function getReasonL10nId(reason, addonType) {
+  let l10nId = `abuse-report-${reason}-reason`;
+  // Special case reasons that have a addonType-specific
+  // l10n id.
+  if (ABUSE_REASONS[reason].hasAddonTypeL10nId) {
+    l10nId += `-${addonType}`;
+  }
+  return l10nId;
+}
+
+function getSuggestionsTemplate(reason, addonType) {
+  const reasonInfo = ABUSE_REASONS[reason];
+  if (!reasonInfo.hasSuggestions) {
+    return null;
+  }
+  let templateId = `tmpl-suggestions-${reason}`;
+  // Special case reasons that have a addonType-specific
+  // suggestion template.
+  if (reasonInfo.hasAddonTypeSuggestionTemplate) {
+    templateId += `-${addonType}`;
+  }
+  return document.getElementById(templateId);
+}
+
+// Map of the learnmore links metadata, keyed by link element class.
+const LEARNMORE_LINKS = {
+  ".abuse-report-learnmore": {
+    path: "reporting-extensions-and-themes-abuse",
+  },
+  ".abuse-settings-search-learnmore": {
+    path: "prefs-search",
+  },
+  ".abuse-settings-homepage-learnmore": {
+    path: "prefs-homepage",
+  },
+  ".abuse-policy-learnmore": {
+    baseURL: "https://www.mozilla.org/%LOCALE%/",
+    path: "about/legal/report-infringement/",
+  },
+};
+
+// Format links that match the selector in the LEARNMORE_LINKS map
+// found in a given container element.
+function formatLearnMoreURLs(containerEl) {
+  for (const [linkClass, linkInfo] of Object.entries(LEARNMORE_LINKS)) {
+    for (const element of containerEl.querySelectorAll(linkClass)) {
+      const baseURL = linkInfo.baseURL ?
+        Services.urlFormatter.formatURL(linkInfo.baseURL) :
+        Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+      element.href = baseURL + linkInfo.path;
+    }
+  }
+}
+
+// Define a set of getters from a Map<propertyName, selector>.
+function defineElementSelectorsGetters(object, propsMap) {
+  const props = Object.entries(propsMap).reduce((acc, entry) => {
+    const [name, selector] = entry;
+    acc[name] = {get: () => object.querySelector(selector)};
+    return acc;
+  }, {});
+  Object.defineProperties(object, props);
+}
+
+// Define a set of properties getters and setters for a
+// Map<propertyName, attributeName>.
+function defineElementAttributesProperties(object, propsMap) {
+  const props = Object.entries(propsMap).reduce((acc, entry) => {
+    const [name, attr] = entry;
+    acc[name] = {
+      get: () => object.getAttribute(attr),
+      set: (value) => {
+        object.setAttribute(attr, value);
+      },
+    };
+    return acc;
+  }, {});
+  Object.defineProperties(object, props);
+}
+
+// Return an object with properties associated to elements
+// found using the related selector in the propsMap.
+function getElements(containerEl, propsMap) {
+  return Object.entries(propsMap).reduce((acc, entry) => {
+    const [name, selector] = entry;
+    let elements = containerEl.querySelectorAll(selector);
+    acc[name] = elements.length > 1 ? elements : elements[0];
+    return acc;
+  }, {});
+}
+
+function dispatchCustomEvent(el, eventName, detail) {
+  el.dispatchEvent(new CustomEvent(eventName, {detail}));
+}
+
+// This WebComponent extends the li item to represent an abuse report reason
+// and it is responsible for:
+// - embedding a photon styled radio buttons
+// - localizing the reason list item
+// - optionally embedding a localized example, positioned
+//   below the reason label, and adjusts the item height
+//   accordingly
+class AbuseReasonListItem extends HTMLLIElement {
+  constructor() {
+    super();
+    defineElementAttributesProperties(this, {
+      addonType: "addon-type",
+      reason: "report-reason",
+      checked: "checked",
+    });
+  }
+
+  connectedCallback() {
+    this.update();
+  }
+
+  async update() {
+    if (this.reason !== "other" && !this.addonType) {
+      return;
+    }
+
+    const {reason, checked, addonType} = this;
+
+    this.textContent = "";
+    const content = document.importNode(this.template.content, true);
+
+    if (reason) {
+      const reasonId = `abuse-reason-${reason}`;
+      const reasonInfo = ABUSE_REASONS[reason] || {};
+
+      const {labelEl, descriptionEl, radioEl} = getElements(content, {
+        labelEl: "label",
+        descriptionEl: ".reason-description",
+        radioEl: "input[type=radio]",
+      });
+
+      labelEl.setAttribute("for", reasonId);
+      radioEl.id = reasonId;
+      radioEl.value = reason;
+      radioEl.checked = !!checked;
+
+      // This reason has a different localized description based on the
+      // addon type.
+      document.l10n.setAttributes(
+        descriptionEl, getReasonL10nId(reason, addonType));
+
+      // Show the reason example if supported for the addon type.
+      if (!reasonInfo.isExampleHidden(addonType)) {
+        const exampleEl = content.querySelector(".reason-example");
+        document.l10n.setAttributes(
+          exampleEl, `abuse-report-${reason}-example`);
+        exampleEl.hidden = false;
+      }
+    }
+
+    formatLearnMoreURLs(content);
+
+    this.appendChild(content);
+  }
+
+  get template() {
+    return document.getElementById("tmpl-reason-listitem");
+  }
+}
+
+// This WebComponents implements the first step of the abuse
+// report submission and embeds a randomized reasons list.
+class AbuseReasonsPanel extends HTMLElement {
+  constructor() {
+    super();
+    defineElementAttributesProperties(this, {
+      addonType: "addon-type",
+    });
+  }
+
+  connectedCallback() {
+    this.update();
+  }
+
+  update() {
+    if (!this.isConnected || !this.addonType) {
+      return;
+    }
+
+    const {addonType} = this;
+
+    this.textContent = "";
+    const content = document.importNode(this.template.content, true);
+
+    const {titleEl, listEl} = getElements(content, {
+      titleEl: ".abuse-report-title",
+      listEl: "ul.abuse-report-reasons",
+    });
+
+    // Change the title l10n-id if the addon type is theme.
+    document.l10n.setAttributes(titleEl, `abuse-report-title-${addonType}`);
+
+    // Create the randomized list of reasons.
+    const reasons = Object.keys(ABUSE_REASONS)
+                          .filter(reason => reason !== "other")
+                          .sort(() => Math.random() - 0.5);
+
+    for (const reason of reasons) {
+      const reasonInfo = ABUSE_REASONS[reason];
+      if (!reasonInfo || reasonInfo.isReasonHidden(addonType)) {
+        // Skip an extension only reason while reporting a theme.
+        continue;
+      }
+      const item = document.createElement("li", {
+        is: "abuse-report-reason-listitem",
+      });
+      item.reason = reason;
+      item.addonType = addonType;
+
+      listEl.prepend(item);
+    }
+
+    listEl.firstElementChild.checked = true;
+    formatLearnMoreURLs(content);
+
+    this.appendChild(content);
+  }
+
+  get template() {
+    return document.getElementById("tmpl-reasons-panel");
+  }
+}
+
+// This WebComponent is responsible for the suggestions, which are:
+// - generated based on a template keyed by abuse report reason
+// - localized by assigning fluent ids generated from the abuse report reason
+// - learn more and extension support url are then generated when the
+//   specific reason expects it
+class AbuseReasonSuggestions extends HTMLElement {
+  constructor() {
+    super();
+    defineElementAttributesProperties(this, {
+      extensionSupportURL: "extension-support-url",
+      reason: "report-reason",
+    });
+  }
+
+  update() {
+    const {addonType, extensionSupportURL, reason} = this;
+
+    if (!addonType) {
+      return;
+    }
+
+    this.textContent = "";
+
+    let template = getSuggestionsTemplate(reason, addonType);
+    if (template) {
+      let content = document.importNode(template.content, true);
+
+      formatLearnMoreURLs(content);
+
+      let extSupportLink = content.querySelector("a.extension-support-link");
+      if (extSupportLink) {
+        extSupportLink.href = extensionSupportURL;
+      }
+
+      this.appendChild(content);
+      this.hidden = false;
+    } else {
+      this.hidden = true;
+    }
+  }
+
+  get LEARNMORE_LINKS() {
+    return Object.keys(LEARNMORE_LINKS);
+  }
+}
+
+// This WebComponents implements the last step of the abuse report submission.
+class AbuseSubmitPanel extends HTMLElement {
+  constructor() {
+    super();
+    defineElementAttributesProperties(this, {
+      addonType: "addon-type",
+      reason: "report-reason",
+      extensionSupportURL: "extensionSupportURL",
+    });
+    defineElementSelectorsGetters(this, {
+      _textarea: "textarea",
+      _title: ".abuse-reason-title",
+      _suggestions: "abuse-report-reason-suggestions",
+    });
+  }
+
+  connectedCallback() {
+    this.render();
+  }
+
+  render() {
+    this.textContent = "";
+    this.appendChild(document.importNode(this.template.content, true));
+  }
+
+  update() {
+    if (!this.isConnected || !this.addonType) {
+      return;
+    }
+    const {addonType, reason, _suggestions, _title} = this;
+    document.l10n.setAttributes(_title, getReasonL10nId(reason, addonType));
+    _suggestions.reason = reason;
+    _suggestions.addonType = addonType;
+    _suggestions.extensionSupportURL = this.extensionSupportURL;
+    _suggestions.update();
+  }
+
+  clear() {
+    this._textarea.value = "";
+  }
+
+  get template() {
+    return document.getElementById("tmpl-submit-panel");
+  }
+}
+
+// This WebComponent provides the abuse report
+class AbuseReport extends HTMLElement {
+  constructor() {
+    super();
+    this._report = null;
+    defineElementSelectorsGetters(this, {
+      _form: "form",
+      _textarea: "textarea",
+      _radioCheckedReason: "[type=radio]:checked",
+      _reasonsPanel: "abuse-report-reasons-panel",
+      _submitPanel: "abuse-report-submit-panel",
+      _reasonsPanelButtons: ".abuse-report-reasons-buttons",
+      _submitPanelButtons: ".abuse-report-submit-buttons",
+      _iconClose: ".abuse-report-close-icon",
+      _btnNext: "button.abuse-report-next",
+      _btnCancel: "button.abuse-report-cancel",
+      _btnGoBack: "button.abuse-report-goback",
+      _btnSubmit: "button.abuse-report-submit",
+      _addonIconElement: ".abuse-report-header img.addon-icon",
+      _addonNameElement: ".abuse-report-header .addon-name",
+      _linkAddonAuthor: ".abuse-report-header .addon-author-box a.author",
+    });
+  }
+
+  connectedCallback() {
+    this.render();
+
+    this.addEventListener("click", this);
+    // Start listening to focus events (to adjust the focused
+    // element during keyboard navigation).
+    document.addEventListener("focus", this, true);
+    // Start listening to keydown events (to close the modal
+    // when Escape has been pressed).
+    document.addEventListener("keydown", this);
+  }
+
+  disconnectedCallback() {
+    this.textContent = "";
+    this.removeEventListener("click", this);
+    document.removeEventListener("focus", this, true);
+    document.removeEventListener("keydown", this);
+  }
+
+  handleEvent(evt) {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+
+    switch (evt.type) {
+      case "focus":
+        if (evt.target === document.body) {
+          // Return the focus to the Firefox UI when the body has been focused
+          // (as it is only reached when navigating back from the last focusable
+          // objects in the abuse report panel).
+          const chromeWin = window.windowRoot.ownerGlobal;
+          // Top level browser's previous sibling.
+          const {
+            previousElementSibling,
+          } = window.parent.docShell.chromeEventHandler;
+          Services.focus.moveFocus(
+            chromeWin, previousElementSibling,
+            Services.focus.MOVE_BACKWARD, Services.focus.FLAG_BYKEY);
+          return;
+        } else if (this.contains(evt.target)) {
+          return;
+        }
+        this.focus();
+        break;
+      case "keydown":
+        if (evt.key === "Escape") {
+          // Prevent Esc to close the panel if the textarea is
+          // empty.
+          if (this.message && !this._submitPanel.hidden) {
+            return;
+          }
+          this.cancel();
+        }
+        break;
+      case "click":
+        if (evt.target === this._iconClose ||
+            evt.target === this._btnCancel) {
+          this.cancel();
+        }
+        if (evt.target === this._btnNext) {
+          this.switchToSubmitMode();
+        }
+        if (evt.target === this._btnGoBack) {
+          this.switchToListMode();
+        }
+        if (evt.target === this._btnSubmit) {
+          this.submit();
+        }
+        break;
+    }
+  }
+
+  render() {
+    this.textContent = "";
+    this.appendChild(document.importNode(this.template.content, true));
+  }
+
+  async update() {
+    if (!this.addon) {
+      return;
+    }
+
+    const {
+      addonId,
+      _addonIconElement,
+      _addonNameElement,
+      _linkAddonAuthor,
+      _reasonsPanel,
+      _submitPanel,
+    } = this;
+
+    // Ensure that the first step of the abuse submission is the one
+    // currently visible.
+    this.switchToListMode();
+
+    // Cancel the abuse report if the addon is not an extension or theme.
+    if (!["extension", "theme"].includes(this.addonType)) {
+      this.cancel();
+      return;
+    }
+
+    _addonNameElement.textContent = this.addonName;
+
+    _linkAddonAuthor.href = this.authorURL || this.homepageURL;
+    _linkAddonAuthor.textContent = this.authorName;
+    document.l10n.setAttributes(_linkAddonAuthor.parentNode,
+                                "abuse-report-addon-authored-by",
+                                {"author-name": this.authorName});
+
+    _addonIconElement.setAttribute("src", this.iconURL);
+
+    _reasonsPanel.addonType = this.addonType;
+    _reasonsPanel.update();
+
+    _submitPanel.addonType = this.addonType;
+    _submitPanel.reason = this.reason;
+    _submitPanel.extensionSupportURL = this.supportURL;
+    _submitPanel.update();
+
+    this.focus();
+    dispatchCustomEvent(this, "abuse-report:updated", {
+      addonId, panel: "reasons",
+    });
+  }
+
+  setAbuseReport(abuseReport) {
+    this._report = abuseReport;
+    // Clear the textarea from any previously entered content.
+    this._submitPanel.clear();
+
+    if (abuseReport) {
+      this.update();
+      this.hidden = false;
+    } else {
+      this.hidden = true;
+    }
+  }
+
+  focus() {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+    if (this._reasonsPanel.hidden) {
+      const {_textarea} = this;
+      _textarea.focus();
+      _textarea.select();
+    } else {
+      const {_radioCheckedReason} = this;
+      if (_radioCheckedReason) {
+        _radioCheckedReason.focus();
+      }
+    }
+  }
+
+  cancel() {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+    this._report = null;
+    dispatchCustomEvent(this, "abuse-report:cancel");
+  }
+
+  submit() {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+    dispatchCustomEvent(this, "abuse-report:submit", {
+      addonId: this.addonId,
+      reason: this.reason,
+      message: this.message,
+      report: this._report,
+    });
+  }
+
+  switchToSubmitMode() {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+    this._submitPanel.reason = this.reason;
+    this._submitPanel.update();
+    this._reasonsPanel.hidden = true;
+    this._reasonsPanelButtons.hidden = true;
+    this._submitPanel.hidden = false;
+    this._submitPanelButtons.hidden = false;
+    // Adjust the focused element when switching to the submit panel.
+    this.focus();
+    dispatchCustomEvent(this, "abuse-report:updated", {
+      addonId: this.addonId, panel: "submit",
+    });
+  }
+
+  switchToListMode() {
+    if (!this.isConnected || !this.addon) {
+      return;
+    }
+    this._submitPanel.hidden = true;
+    this._submitPanelButtons.hidden = true;
+    this._reasonsPanel.hidden = false;
+    this._reasonsPanelButtons.hidden = false;
+    // Adjust the focused element when switching back to the list of reasons.
+    this.focus();
+    dispatchCustomEvent(this, "abuse-report:updated", {
+      addonId: this.addonId, panel: "reasons",
+    });
+  }
+
+  get addon() {
+    return this._report && this._report.addon;
+  }
+
+  get addonId() {
+    return this.addon && this.addon.id;
+  }
+
+  get addonName() {
+    return this.addon && this.addon.name;
+  }
+
+  get addonType() {
+    return this.addon && this.addon.type;
+  }
+
+  get addonCreator() {
+    return this.addon && this.addon.creator;
+  }
+
+  get homepageURL() {
+    const {addon} = this;
+    return addon && addon.homepageURL || this.authorURL;
+  }
+
+  get authorName() {
+    // The author name may be missing on some of the test extensions
+    // (or for temporarily installed add-ons).
+    return this.addonCreator && this.addonCreator.name || "";
+  }
+
+  get authorURL() {
+    return this.addonCreator && this.addonCreator.url;
+  }
+
+  get iconURL() {
+    return this.addon && this.addon.iconURL;
+  }
+
+  get supportURL() {
+    return this.addon && this.addon.supportURL || this.homepageURL;
+  }
+
+  get message() {
+    return this._form.elements.message.value;
+  }
+
+  get reason() {
+    return this._form.elements.reason.value;
+  }
+
+  get template() {
+    return document.getElementById("tmpl-abuse-report");
+  }
+}
+
+customElements.define("abuse-report-reason-listitem",
+                      AbuseReasonListItem, {extends: "li"});
+customElements.define("abuse-report-reason-suggestions",
+                      AbuseReasonSuggestions);
+customElements.define("abuse-report-reasons-panel", AbuseReasonsPanel);
+customElements.define("abuse-report-submit-panel", AbuseSubmitPanel);
+customElements.define("addon-abuse-report", AbuseReport);
+
+window.addEventListener("load", () => {
+  document.body.prepend(document.createElement("addon-abuse-report"));
+}, {once: true});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-reports.js
@@ -0,0 +1,182 @@
+/* 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/. */
+
+/* eslint max-len: ["error", 80] */
+
+/**
+ * This script is part of the HTML about:addons page and it provides some
+ * helpers used for the Abuse Reporting submission (and related message bars).
+ */
+
+// Message Bars definitions.
+const ABUSE_REPORT_MESSAGE_BARS = {
+  // Idle message-bar (used while the submission is still ongoing).
+  "submitting": {id: "submitting", actions: ["cancel"]},
+  // Submitted report message-bar.
+  "submitted": {
+    id: "submitted", actions: ["remove", "keep"], dismissable: true,
+  },
+  // Submitted report and remove addon message-bar.
+  "submitted-and-removed": {
+    id: "removed", addonTypeSuffix: true, dismissable: true,
+  },
+  // The "aborted report" message bar is rendered as a generic informative one,
+  // because aborting a report is triggered by a user choice.
+  "ERROR_ABORTED_SUBMIT": {
+    id: "aborted", type: "generic", dismissable: true,
+  },
+  // Errors message bars.
+  "ERROR_ADDON_NOTFOUND": {
+    id: "error", type: "error", dismissable: true,
+  },
+  "ERROR_CLIENT": {
+    id: "error", type: "error", dismissable: true,
+  },
+  "ERROR_NETWORK": {
+    id: "error", actions: ["retry", "cancel"], type: "error",
+  },
+  "ERROR_RECENT_SUBMIT": {
+    id: "error-recent-submit", actions: ["retry", "cancel"], type: "error",
+  },
+  "ERROR_SERVER": {
+    id: "error", actions: ["retry", "cancel"], type: "error",
+  },
+  "ERROR_UNKNOWN": {
+    id: "error", actions: ["retry", "cancel"], type: "error",
+  },
+};
+
+// Helper function used to create abuse report message bars in the
+// HTML about:addons page.
+function createReportMessageBar(
+  definitionId, {addonId, addonName, addonType},
+  {onclose, onaction} = {}
+) {
+  const getMessageL10n = (id) => `abuse-report-messagebar-${id}`;
+  const getActionL10n = (action) => getMessageL10n(`action-${action}`);
+
+  const barInfo = ABUSE_REPORT_MESSAGE_BARS[definitionId];
+  if (!barInfo) {
+    throw new Error(`message-bar definition not found: ${definitionId}`);
+  }
+  const {id, dismissable, actions, type, addonTypeSuffix} = barInfo;
+  const messageEl = document.createElement("span");
+
+  // The message element includes an addon-name span (also filled by
+  // Fluent), which can be used to apply custom styles to the addon name
+  // included in the message bar (if needed).
+  const addonNameEl = document.createElement("span");
+  addonNameEl.setAttribute("data-l10n-name", "addon-name");
+  messageEl.append(addonNameEl);
+
+  document.l10n.setAttributes(
+    messageEl,
+    getMessageL10n(addonTypeSuffix ? `${id}-${addonType}` : id),
+    {"addon-name": addonName || addonId});
+
+  const barActions = actions ? actions.map(action => {
+    const buttonEl = document.createElement("button");
+    buttonEl.addEventListener("click", () => onaction && onaction(action));
+    document.l10n.setAttributes(buttonEl, getActionL10n(action));
+    return buttonEl;
+  }) : [];
+
+  const messagebar = document.createElement("message-bar");
+  messagebar.setAttribute("type", type || "generic");
+  if (dismissable) {
+    messagebar.setAttribute("dismissable", "");
+  }
+  messagebar.append(messageEl, ...barActions);
+  messagebar.addEventListener("message-bar:close", onclose, {once: true});
+
+  document.getElementById("abuse-reports-messages").append(messagebar);
+
+  document.dispatchEvent(new CustomEvent("abuse-report:new-message-bar", {
+    detail: {definitionId, messagebar},
+  }));
+  return messagebar;
+}
+
+async function submitReport({report, reason, message}) {
+  const addonId = report.addon.id;
+  const addonName = report.addon && report.addon.name;
+  const addonType = report.addon && report.addon.type;
+
+  // Create a message bar while we are still submitting the report.
+  const mbSubmitting = createReportMessageBar(
+    "submitting", {addonId, addonName, addonType}, {
+      onaction: (action) => {
+        if (action === "cancel") {
+          report.abort();
+          mbSubmitting.remove();
+        }
+      },
+    });
+
+  try {
+    await report.submit({reason, message});
+    mbSubmitting.remove();
+
+    // Create a submitted message bar is the submission has been
+    // successful.
+    // With reportEntryPoint "uninstall" a specific message bar
+    // is going to be used. All the other reportEntryPoint
+    // values ("menu" and "toolbar_context_menu") uses the same
+    // "submitted" message bar.
+    const barId = report.reportEntryPoint === "uninstall" ?
+      "submitted-and-removed" : "submitted";
+
+    const mbInfo = createReportMessageBar(barId, {
+      addonId, addonName, addonType,
+    }, {
+      onaction: (action) => {
+        mbInfo.remove();
+        // action "keep" doesn't require any further action,
+        // just handle "remove".
+        if (action === "remove") {
+          report.addon.uninstall();
+        }
+      },
+    });
+  } catch (err) {
+    // Log the complete error in the console.
+    console.error("Error submitting abuse report for", addonId, err);
+    mbSubmitting.remove();
+    // The report has a submission error, create a error message bar which
+    // may optionally allow the user to retry to submit the same report.
+    const barId = err.errorType in ABUSE_REPORT_MESSAGE_BARS ?
+      err.errorType : "ERROR_UNKNOWN";
+
+    const mbError = createReportMessageBar(barId, {
+      addonId, addonName, addonType,
+    }, {
+      onaction: (action) => {
+        mbError.remove();
+        switch (action) {
+          case "retry":
+            submitReport({report, reason, message});
+            break;
+          case "cancel":
+            report.abort();
+            break;
+        }
+      },
+    });
+  }
+}
+
+document.addEventListener("abuse-report:submit", ({detail}) => {
+  submitReport(detail);
+});
+
+document.addEventListener("abuse-report:create-error", ({detail}) => {
+  const {addonId, addon, errorType} = detail;
+  const barId = errorType in ABUSE_REPORT_MESSAGE_BARS ?
+    errorType : "ERROR_UNKNOWN";
+  createReportMessageBar(barId, {
+    addonId,
+    addonName: addon && addon.name,
+    addonType: addon && addon.type,
+  });
+});
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -24,16 +24,17 @@
   <linkset>
     <xhtml:link rel="localization" href="branding/brand.ftl"/>
     <xhtml:link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
   </linkset>
   
   <script src="chrome://global/content/contentAreaUtils.js"/>
   <script src="chrome://mozapps/content/extensions/aboutaddonsCommon.js"/>
   <script src="chrome://mozapps/content/extensions/extensions.js"/>
+  <script src="chrome://mozapps/content/extensions/abuse-report-frame.js"/>
 
   <popupset>
     <!-- menu for an addon item -->
     <menupopup id="addonitem-popup">
       <menuitem id="menuitem_showDetails" command="cmd_showItemDetails"
                 default="true" data-l10n-id="cmd-show-details"/>
       <menuitem id="menuitem_enableItem" command="cmd_enableItem"
                 label="&cmd.enableAddon.label;"
@@ -125,17 +126,19 @@
     <command id="cmd_alwaysActivateItem"/>
     <command id="cmd_neverActivateItem"/>
   </commandset>
 
   <keyset>
     <key id="focusSearch" data-l10n-id="search-header-shortcut"
          modifiers="accel" command="cmd_focusSearch"/>
   </keyset>
-  <hbox flex="1">
+
+  <stack id="main-page-stack" flex="1">
+  <hbox id="main-page-content" flex="1">
     <vbox id="category-box">
       <!-- category list -->
       <richlistbox id="categories" flex="1">
         <richlistitem id="category-discover" value="addons://discover/"
                       class="category"
                       data-l10n-id="extensions-view-discover"
                       data-l10n-attrs="name"
                       priority="1000"/>
@@ -666,9 +669,11 @@
             </scrollbox>
 
             <browser id="html-view-browser" type="content" flex="1" disablehistory="true"/>
           </deck>
         </vbox>
       </deck>
     </vbox>
   </hbox>
+  <addon-abuse-report-xulframe hidden="true"></addon-abuse-report-xulframe>
+  </stack>
 </page>
--- a/toolkit/mozapps/extensions/jar.mn
+++ b/toolkit/mozapps/extensions/jar.mn
@@ -17,13 +17,18 @@ toolkit.jar:
   content/mozapps/extensions/blocklist.js                       (content/blocklist.js)
   content/mozapps/extensions/pluginPrefs.xul                    (content/pluginPrefs.xul)
   content/mozapps/extensions/pluginPrefs.js                     (content/pluginPrefs.js)
   content/mozapps/extensions/OpenH264-license.txt               (content/OpenH264-license.txt)
   content/mozapps/extensions/aboutaddons.html                   (content/aboutaddons.html)
   content/mozapps/extensions/aboutaddons.js                     (content/aboutaddons.js)
   content/mozapps/extensions/aboutaddonsCommon.js               (content/aboutaddonsCommon.js)
   content/mozapps/extensions/aboutaddons.css                    (content/aboutaddons.css)
+  content/mozapps/extensions/abuse-reports.js                   (content/abuse-reports.js)
+  content/mozapps/extensions/abuse-report-frame.html            (content/abuse-report-frame.html)
+  content/mozapps/extensions/abuse-report-frame.js              (content/abuse-report-frame.js)
+  content/mozapps/extensions/abuse-report-panel.css             (content/abuse-report-panel.css)
+  content/mozapps/extensions/abuse-report-panel.js              (content/abuse-report-panel.js)
   content/mozapps/extensions/message-bar.css                    (content/message-bar.css)
   content/mozapps/extensions/message-bar.js                     (content/message-bar.js)
   content/mozapps/extensions/panel-list.css                     (content/panel-list.css)
   content/mozapps/extensions/panel-item.css                     (content/panel-item.css)
 #endif
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -70,16 +70,17 @@ skip-if = os == "linux" && !debug # Bug 
 [browser_dragdrop.js]
 [browser_dragdrop_incompat.js]
 [browser_extension_sideloading_permission.js]
 [browser_file_xpi_no_process_switch.js]
 skip-if = true # Bug 1449071 - Frequent failures
 [browser_globalwarnings.js]
 [browser_gmpProvider.js]
 skip-if = os == 'linux' && !debug # Bug 1398766
+[browser_html_abuse_report.js]
 [browser_html_detail_view.js]
 [browser_html_list_view.js]
 [browser_html_message_bar.js]
 [browser_html_plugins.js]
 skip-if = (os == 'win' && processor == 'aarch64') # aarch64 has no plugin support, bug 1525174 and 1547495
 [browser_html_recent_updates.js]
 [browser_html_updates.js]
 [browser_inlinesettings_browser.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
@@ -0,0 +1,816 @@
+/* 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/. */
+/* eslint max-len: ["error", 80] */
+
+const {
+  AbuseReporter,
+} = ChromeUtils.import("resource://gre/modules/AbuseReporter.jsm");
+const {
+  AddonTestUtils,
+} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+
+const ADDON_ID = "test-extension-to-report@mochi.test";
+const REPORT_ENTRY_POINT = "test-entrypoint";
+const BASE_TEST_MANIFEST = {
+  name: "Fake extension to report",
+  author: "Fake author",
+  homepage_url: "https://fake.extension.url/",
+  applications: {gecko: {id: ADDON_ID}},
+  icons: {
+    32: "test-icon.png",
+  },
+};
+
+let gHtmlAboutAddonsWindow;
+let gManagerWindow;
+let apiRequestHandler;
+
+AddonTestUtils.initMochitest(this);
+
+// Init test report api server.
+const server = AddonTestUtils.createHttpServer({hosts: ["test.addons.org"]});
+server.registerPathHandler("/api/report/", (request, response) => {
+  const stream = request.bodyInputStream;
+  const buffer = NetUtil.readInputStream(stream, stream.available());
+  const data = new TextDecoder().decode(buffer);
+  apiRequestHandler({data, request, response});
+});
+
+function handleSubmitRequest({request, response}) {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "application/json", false);
+  response.write("{}");
+}
+
+async function openAboutAddons(type = "extension") {
+  const win = await loadInitialView(type);
+  gHtmlAboutAddonsWindow = win;
+  gManagerWindow = win.managerWindow;
+}
+
+async function closeAboutAddons() {
+  if (gHtmlAboutAddonsWindow) {
+    await closeView(gHtmlAboutAddonsWindow);
+    gHtmlAboutAddonsWindow = null;
+    gManagerWindow = null;
+  }
+}
+
+async function installTestExtension(
+  id = ADDON_ID, type = "extension", manifest = {}
+) {
+  const additionalProps = type === "theme" ? {
+    theme: {
+      colors: {
+        frame: "#a14040",
+        tab_background_text: "#fac96e",
+      },
+    },
+  } : {};
+  const extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      ...BASE_TEST_MANIFEST,
+      ...additionalProps,
+      ...manifest,
+      applications: {gecko: {id}},
+    },
+    useAddonManager: "temporary",
+  });
+  await extension.startup();
+  return extension;
+}
+
+function getAbuseReportFrame() {
+  return gManagerWindow.document.querySelector(
+    "addon-abuse-report-xulframe");
+}
+
+function getAbuseReasons(abuseReportEl) {
+  return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS);
+}
+
+function getAbuseReasonInfo(abuseReportEl, reason) {
+  return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason];
+}
+
+function triggerNewAbuseReport(addonId, reportEntryPoint) {
+  const el = getAbuseReportFrame();
+  el.ownerGlobal.openAbuseReport({addonId, reportEntryPoint});
+}
+
+function triggerSubmitAbuseReport(reason, message) {
+  const el = getAbuseReportFrame();
+  el.handleEvent(new CustomEvent("abuse-report:submit", {
+    detail: {report: el.report, reason, message},
+  }));
+}
+
+async function openAbuseReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) {
+  // Close the current about:addons window if it has been leaved open from
+  // a previous test case failure.
+  if (gHtmlAboutAddonsWindow) {
+    await closeAboutAddons();
+  }
+
+  await openAboutAddons();
+  const el = getAbuseReportFrame();
+
+  const onceUpdated = BrowserTestUtils.waitForEvent(el, "abuse-report:updated");
+
+  triggerNewAbuseReport(addonId, reportEntryPoint);
+
+  // The abuse report panel is collecting the report metadata and rendering
+  // asynchronously, await on the "abuse-report:updated" event to be sure that
+  // it has been rendered.
+  const evt = await onceUpdated;
+  is(evt.detail.addonId, addonId, `Abuse Report panel updated for ${addonId}`);
+
+  return el.promiseAbuseReport;
+}
+
+async function promiseAbuseReportRendered(abuseReportEl) {
+  let el = abuseReportEl;
+  if (!el) {
+    const frame = getAbuseReportFrame();
+    el = await frame.promiseAbuseReport;
+  }
+  return el._radioCheckedReason ? Promise.resolve() :
+    BrowserTestUtils.waitForEvent(el, "abuse-report:updated",
+                                  "Wait the abuse report panel to be rendered");
+}
+
+async function promiseAbuseReportUpdated(abuseReportEl, panel) {
+  const evt = await BrowserTestUtils.waitForEvent(
+    abuseReportEl, "abuse-report:updated",
+    "Wait abuse report panel update");
+
+  if (panel) {
+    is(evt.detail.panel, panel, `Got a "${panel}" update event`);
+    switch (evt.detail.panel) {
+      case "reasons":
+        ok(!abuseReportEl._reasonsPanel.hidden,
+           "Reasons panel should be visible");
+        ok(abuseReportEl._submitPanel.hidden,
+           "Submit panel should be hidden");
+        break;
+      case "submit":
+        ok(abuseReportEl._reasonsPanel.hidden,
+           "Reasons panel should be hidden");
+        ok(!abuseReportEl._submitPanel.hidden,
+           "Submit panel should be visible");
+        break;
+    }
+  }
+}
+
+function promiseMessageBars(expectedMessageBarCount) {
+  return new Promise(resolve => {
+    const details = [];
+    function listener(evt) {
+      details.push(evt.detail);
+      if (details.length >= expectedMessageBarCount) {
+        cleanup();
+        resolve(details);
+      }
+    }
+    function cleanup() {
+      if (gHtmlAboutAddonsWindow) {
+        gHtmlAboutAddonsWindow.document.removeEventListener(
+          "abuse-report:new-message-bar", listener);
+      }
+    }
+    gHtmlAboutAddonsWindow.document.addEventListener(
+      "abuse-report:new-message-bar", listener);
+  });
+}
+
+add_task(async function setup() {
+  // Enable html about:addons and the abuse reporting.
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["extensions.htmlaboutaddons.enabled", true],
+      ["extensions.abuseReport.enabled", true],
+      ["extensions.abuseReport.url", "http://test.addons.org/api/report/"],
+    ],
+  });
+});
+
+// This test case verifies that:
+// - the about:addons XUL page contains the addon-abuse-report-xulframe element,
+//   and that
+//   it is part of a XUL stack alongside with the rest of a about:addons
+//   XUL page
+// - the addon-abuse-report-xulframe contains a transparent browser XUL element
+//   and it is shown and hidden as expected when its addon-id attribute is set
+//   and then removed
+// - the addon-abuse-report-xulframe move the focus to the abuse-report-panel
+//   embedded into it,
+// - it is automatically hidden when the abuse-report-panel has been closed
+//   by the user, and the focus is returned back to the about:addons page.
+add_task(async function addon_abusereport_xulframe() {
+  const extension = await installTestExtension();
+
+  const addon = await AddonManager.getAddonByID(ADDON_ID);
+  ok(addon, "The test add-on has been found");
+
+  await openAboutAddons();
+  const el = getAbuseReportFrame();
+
+  // Verify that the abuse report XUL WebComponent is positioned in the
+  // XUL about:addons page at the expected position.
+  ok(el, "Got an addon-abuse-report-xulframe element in the about:addons page");
+  is(el.parentNode.tagName, "stack", "Got the expected parent element");
+  is(el.previousElementSibling.tagName, "hbox",
+     "Got the expected previous sibling element");
+  is(el.parentNode.lastElementChild, el,
+     "The addon-abuse-report-xulframe is the last element of the XUL stack");
+  ok(!el.hasAttribute("addon-id"),
+     "The addon-id attribute should be initially empty");
+
+  // Set the addon-id attribute and check that the abuse report elements is
+  // being shown.
+  const onceUpdated = BrowserTestUtils.waitForEvent(el, "abuse-report:updated");
+  el.openReport({addonId: ADDON_ID, reportEntryPoint: "test"});
+
+  // Wait the abuse report to be loaded.
+  const abuseReportEl = await el.promiseAbuseReport;
+  await onceUpdated;
+
+  ok(!el.hidden, "The addon-abuse-report-xulframe element is visible");
+  is(el.getAttribute("addon-id"), ADDON_ID,
+     "Got the expected addon-id attribute set on the frame element");
+  is(el.getAttribute("report-entry-point"), "test",
+     "Got the expected report-entry-point attribute set on the frame element");
+
+  const browser = el.querySelector("browser");
+
+  is(gManagerWindow.document.activeElement, browser,
+     "The addon-abuse-report-xulframe has been focused");
+  ok(browser, "The addon-abuse-report-xulframe contains a XUL browser element");
+  is(browser.getAttribute("transparent"), "true",
+     "The XUL browser element is transparent as expected");
+
+  ok(abuseReportEl, "Got an addon-abuse-report element");
+  is(abuseReportEl.addonId, ADDON_ID,
+     "The addon-abuse-report element has the expected addonId property");
+  ok(browser.contentDocument.contains(abuseReportEl),
+     "The addon-abuse-report element is part of the embedded XUL browser");
+
+  await extension.unload();
+  await closeAboutAddons();
+});
+
+// Test addon-abuse-xulframe auto hiding scenario.
+add_task(async function addon_abusereport_xulframe_hiding() {
+  const extension = await installTestExtension();
+
+  const addon = await AddonManager.getAddonByID(ADDON_ID);
+  const abuseReportEl = await openAbuseReport(extension.id);
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  const el = getAbuseReportFrame();
+  ok(!el.hidden, "The addon-abuse-report-xulframe element is visible");
+
+  const browser = el.querySelector("browser");
+
+  async function assertAbuseReportFrameHidden(actionFn, msg) {
+    info(`Test ${msg}`);
+
+    const panelEl = await openAbuseReport(addon.id);
+    const frameEl = getAbuseReportFrame();
+
+    ok(!frameEl.hidden, "The abuse report frame is visible");
+
+    const onceFrameHidden = BrowserTestUtils.waitForEvent(
+      frameEl, "abuse-report:frame-hidden");
+    await actionFn({frameEl, panelEl});
+    await onceFrameHidden;
+
+    ok(!panelEl.hasAttribute("addon-id"),
+       "addon-id attribute removed from the addon-abuse-report element");
+
+    ok(gManagerWindow.document.activeElement != browser,
+       "addon-abuse-report-xulframe returned focus back to about:addons");
+
+    await closeAboutAddons();
+  }
+
+  const TESTS = [
+    [
+      async ({panelEl}) => {
+        panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel"));
+      },
+      "addon report panel hidden on abuse-report:cancel event",
+    ],
+    [
+      async () => {
+        await EventUtils.synthesizeKey("KEY_Escape", {},
+                                       abuseReportEl.ownerGlobal);
+      },
+      "addon report panel hidden on Escape key pressed in the xulframe window",
+    ],
+    [
+      async () => {
+        await EventUtils.synthesizeKey("KEY_Escape", {}, gManagerWindow);
+      },
+      "addon report panel hidden on Escape key pressed about:addons window",
+    ],
+    [
+      async ({panelEl}) => {
+        await EventUtils.synthesizeMouseAtCenter(panelEl._iconClose, {},
+                                                 panelEl.ownerGlobal);
+      },
+      "addon report panel hidden on close icon click",
+    ],
+    [
+      async ({panelEl}) => {
+        await EventUtils.synthesizeMouseAtCenter(panelEl._btnCancel, {},
+                                                 panelEl.ownerGlobal);
+      },
+      "addon report panel hidden on close button click",
+    ],
+  ];
+
+  for (const test of TESTS) {
+    await assertAbuseReportFrameHidden(...test);
+  }
+
+  await extension.unload();
+});
+
+// This test case verifies that the expected addon metadata have been
+// set in the abuse report panel, and they gets refreshed as expected
+// when it is reused to report another extension.
+add_task(async function test_abusereport_panel_refresh() {
+  const EXT_ID1 = "test-panel-refresh@mochi.test";
+  const EXT_ID2 = "test-panel-refresh-2@mochi.test";
+  let addon;
+  let extension;
+
+  async function getAbuseReportForManifest(addonId, manifest) {
+    extension = await installTestExtension(addonId, "extension", manifest);
+
+    addon = await AddonManager.getAddonByID(extension.id);
+    ok(addon, "The test add-on has been found");
+
+    await addon.uninstall(true);
+
+    return openAbuseReport(extension.id);
+  }
+
+  function assertExtensionMetadata(panel, expected) {
+    let name = panel.querySelector(".addon-name").textContent;
+    let authorLinkEl = reportPanel.querySelector("a.author");
+    Assert.deepEqual({
+      name,
+      author: authorLinkEl.textContent.trim(),
+      homepage_url: authorLinkEl.getAttribute("href"),
+      icon_url: panel.querySelector(".addon-icon").getAttribute("src"),
+    }, expected, "Got the expected addon metadata");
+  }
+
+  let reportPanel = await getAbuseReportForManifest(EXT_ID1);
+  let {name, author, homepage_url} = BASE_TEST_MANIFEST;
+
+  assertExtensionMetadata(reportPanel, {
+    name, author, homepage_url,
+    icon_url: addon.iconURL,
+  });
+
+  await addon.cancelUninstall();
+  await extension.unload();
+  await closeAboutAddons();
+
+  const extData2 = {
+    name: "Test extension 2",
+    developer: {
+      name: "The extension developer",
+      url: "http://the.extension.url",
+    },
+  };
+  reportPanel = await getAbuseReportForManifest(EXT_ID2, extData2);
+
+  assertExtensionMetadata(reportPanel, {
+    name: extData2.name,
+    author: extData2.developer.name,
+    homepage_url: extData2.developer.url,
+    icon_url: addon.iconURL,
+  });
+
+  const allButtons = Array.from(reportPanel.querySelectorAll("buttons"));
+  ok(allButtons.every(el => el.hasAttribute("data-l10n-id")),
+     "All the panel buttons have a data-l10n-id");
+
+  await addon.cancelUninstall();
+  await extension.unload();
+  await closeAboutAddons();
+});
+
+// This test case verified that the abuse report panels contains a radio
+// button for all the expected "abuse report reasons", they are grouped
+// together under the same form field named "reason".
+add_task(async function test_abusereport_issuelist() {
+  const extension = await installTestExtension();
+
+  const abuseReportEl = await openAbuseReport(extension.id);
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  const reasonsPanel = abuseReportEl._reasonsPanel;
+  const radioButtons = reasonsPanel.querySelectorAll("[type=radio]");
+  const selectedRadios = reasonsPanel.querySelectorAll("[type=radio]:checked");
+
+  is(selectedRadios.length, 1, "Expect only one radio button selected");
+  is(selectedRadios[0], radioButtons[0],
+     "Expect the first radio button to be selected");
+
+  is(abuseReportEl.reason, radioButtons[0].value,
+     `The reason property has the expected value: ${radioButtons[0].value}`);
+
+  const reasons = Array.from(radioButtons).map(el => el.value);
+  Assert.deepEqual(reasons.sort(), getAbuseReasons(abuseReportEl).sort(),
+                   `Got a radio button for the expected reasons`);
+
+  for (const radio of radioButtons) {
+    const reasonInfo = getAbuseReasonInfo(abuseReportEl, radio.value);
+    const expectExampleHidden = reasonInfo &&
+                                reasonInfo.isExampleHidden("extension");
+    is(radio.parentNode.querySelector(".reason-example").hidden,
+       expectExampleHidden,
+       `Got expected visibility on the example for reason "${radio.value}"`);
+  }
+
+  info("Change the selected reason to " + radioButtons[3].value);
+  radioButtons[3].checked = true;
+  is(abuseReportEl.reason, radioButtons[3].value,
+     "The reason property has the expected value");
+
+  await extension.unload();
+  await closeAboutAddons();
+});
+
+// This test case verifies that the abuse report panel:
+// - switches from its "reasons list" mode to its "submit report" mode when the
+//   "next" button is clicked
+// - goes back to the "reasons list" mode when the "go back" button is clicked
+// - the abuse report panel is closed when the "close" icon is clicked
+add_task(async function test_abusereport_submitpanel() {
+  const extension = await installTestExtension();
+
+  const abuseReportEl = await openAbuseReport(extension.id);
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  ok(!abuseReportEl._reasonsPanel.hidden,
+     "The list of abuse reasons is the currently visible");
+  ok(abuseReportEl._submitPanel.hidden,
+    "The submit panel is the currently hidden");
+
+  info("Clicking the 'next' button");
+  let onceUpdated = promiseAbuseReportUpdated(abuseReportEl, "submit");
+  EventUtils.synthesizeMouseAtCenter(abuseReportEl._btnNext, {},
+    abuseReportEl.ownerGlobal);
+  await onceUpdated;
+
+  info("Clicking the 'go back' button");
+  onceUpdated = promiseAbuseReportUpdated(abuseReportEl, "reasons");
+
+  EventUtils.synthesizeMouseAtCenter(abuseReportEl._btnGoBack, {},
+                                     abuseReportEl.ownerGlobal);
+  await onceUpdated;
+
+  info("Clicking the 'close' icon");
+  const onceCancelEvent = BrowserTestUtils.waitForEvent(abuseReportEl,
+                                                        "abuse-report:cancel");
+  EventUtils.synthesizeMouseAtCenter(abuseReportEl._iconClose, {},
+                                     abuseReportEl.ownerGlobal);
+  await onceCancelEvent;
+
+  const frameEl = getAbuseReportFrame();
+
+  ok(frameEl.hidden, "The abuse report frame has been hidden");
+  ok(!frameEl.hasAttribute("addon-id"), "addon-id attribute has been removed");
+
+  await extension.unload();
+  await closeAboutAddons();
+});
+
+// This test case verifies that the abuse report panel sends the expected data
+// in the "abuse-report:submit" event detail.
+add_task(async function test_abusereport_submit() {
+  // Reset the timestamp of the last report between tests.
+  AbuseReporter._lastReportTimestamp = null;
+  const extension = await installTestExtension();
+
+  const abuseReportEl = await openAbuseReport(extension.id);
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  ok(!abuseReportEl._reasonsPanel.hidden,
+     "The list of abuse reasons is the currently visible");
+
+  info("Clicking the 'next' button");
+  let onceUpdated = promiseAbuseReportUpdated(abuseReportEl, "submit");
+  EventUtils.synthesizeMouseAtCenter(abuseReportEl._btnNext, {},
+                                     abuseReportEl.ownerGlobal);
+  await onceUpdated;
+
+  is(abuseReportEl.message, "", "The abuse report message is initially empty");
+
+  info("Test typing a message in the abuse report submit panel textarea");
+  const typedMessage = "Description of the extension abuse report";
+  await EventUtils.synthesizeCompositionChange({
+    composition: {
+      string: typedMessage,
+      clauses: [{
+        length: typedMessage.length,
+        attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
+      }],
+    },
+  });
+
+  is(abuseReportEl.message, typedMessage,
+     "Got the expected typed message in the abuse report");
+
+  const expectedDetail = {
+    addonId: extension.id,
+    reason: abuseReportEl.reason,
+    message: abuseReportEl.message,
+  };
+
+  let reportSubmitted;
+  const onReportSubmitted = new Promise(resolve => {
+    apiRequestHandler = ({data, request, response}) => {
+      reportSubmitted = JSON.parse(data);
+      handleSubmitRequest({request, response});
+      resolve();
+    };
+  });
+
+  info("Clicking the 'submit' button");
+  const onMessageBarsCreated = promiseMessageBars(2);
+
+  const onceSubmitEvent = BrowserTestUtils.waitForEvent(abuseReportEl,
+                                                        "abuse-report:submit");
+  EventUtils.synthesizeMouseAtCenter(abuseReportEl._btnSubmit, {},
+                                     abuseReportEl.ownerGlobal);
+  const submitEvent = await onceSubmitEvent;
+
+  const actualDetail = {
+    addonId: submitEvent.detail.addonId,
+    reason: submitEvent.detail.reason,
+    message: submitEvent.detail.message,
+  };
+  Assert.deepEqual(
+    actualDetail, expectedDetail,
+    "Got the expected detail in the abuse-report:submit event");
+
+  ok(submitEvent.detail.report,
+     "Got a report object in the abuse-report:submit event detail");
+
+  const frameEl = getAbuseReportFrame();
+
+  info("Wait the report to be submitted to the api server");
+  await onReportSubmitted;
+
+  // Verify that, when the "abuse-report:submit" has been sent, the abuse report
+  // panel has been hidden, the report has been submitted and the expected
+  // message bar is created in the HTMl about addons page.
+  ok(frameEl.hidden, "abuse report frame should be hidden");
+  ok(!frameEl.hasAttribute("addon-id"),
+     "addon-id attribute has been removed from the abuse report frame");
+  ok(abuseReportEl.hidden, "abuse report panel should be hidden");
+
+  is(reportSubmitted.addon, ADDON_ID,
+     "Got the expected addon in the submitted report");
+  is(reportSubmitted.reason, expectedDetail.reason,
+     "Got the expected reason in the submitted report");
+  is(reportSubmitted.message, expectedDetail.message,
+     "Got the expected message in the submitted report");
+  is(reportSubmitted.report_entry_point, REPORT_ENTRY_POINT,
+     "Got the expected report_entry_point in the submitted report");
+
+  info("Waiting the expected message bars to be created");
+  const barDetails = await onMessageBarsCreated;
+  is(barDetails.length, 2, "Expect two message bars to have been created");
+  is(barDetails[0].definitionId, "submitting",
+     "Got a submitting message bar as expected");
+  is(barDetails[1].definitionId, "submitted",
+     "Got a submitted message bar as expected");
+
+  await extension.unload();
+  await closeAboutAddons();
+});
+
+// This test case verifies that the abuse report panel contains the expected
+// suggestions when the selected reason requires it (and urls are being set
+// on the links elements included in the suggestions when expected).
+async function test_abuse_report_suggestions(addonId) {
+  const addon = await AddonManager.getAddonByID(addonId);
+
+  const abuseReportEl = await openAbuseReport(addonId);
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  const {
+    _btnNext,
+    _btnGoBack,
+    _reasonsPanel,
+    _submitPanel,
+    _submitPanel: {
+      _suggestions,
+    },
+  } = abuseReportEl;
+
+  for (const reason of getAbuseReasons(abuseReportEl)) {
+    const reasonInfo = getAbuseReasonInfo(abuseReportEl, reason);
+
+    if (reasonInfo.isReasonHidden(addon.type)) {
+      continue;
+    }
+
+    info(`Test suggestions for abuse reason "${reason}"`);
+
+    // Select a reason with suggestions.
+    let radioEl = abuseReportEl.querySelector(`#abuse-reason-${reason}`);
+    ok(radioEl, `Found radio button for "${reason}"`);
+    radioEl.checked = true;
+
+    info("Clicking the 'next' button");
+    let oncePanelUpdated = promiseAbuseReportUpdated(abuseReportEl, "submit");
+    EventUtils.synthesizeMouseAtCenter(_btnNext, {}, _btnNext.ownerGlobal);
+    await oncePanelUpdated;
+
+    const localizedSuggestionsContent = Array.from(
+      _suggestions.querySelectorAll("[data-l10n-id]")
+    ).filter(el => !el.hidden);
+
+    is(!_suggestions.hidden, !!reasonInfo.hasSuggestions,
+       `Suggestions block has the expected visibility for "${reason}"`);
+    if (reasonInfo.hasSuggestions) {
+      ok(localizedSuggestionsContent.length > 0,
+         `Category suggestions should not be empty for "${reason}"`);
+    } else {
+      ok(localizedSuggestionsContent.length === 0,
+         `Category suggestions should be empty for "${reason}"`);
+    }
+
+    const extSupportLink = _suggestions.querySelector(
+      ".extension-support-link");
+    if (extSupportLink) {
+      is(extSupportLink.getAttribute("href"), BASE_TEST_MANIFEST.homepage_url,
+         "Got the expected extension-support-url");
+    }
+
+    const learnMoreLinks = [];
+    for (const linkClass of _suggestions.LEARNMORE_LINKS) {
+      learnMoreLinks.push(..._suggestions.querySelectorAll(linkClass));
+    }
+
+    if (learnMoreLinks.length > 0) {
+      ok(learnMoreLinks.every(el => el.getAttribute("target") === "_blank"),
+         "All the learn more links have target _blank");
+      ok(learnMoreLinks.every(el => el.hasAttribute("href")),
+         "All the learn more links have a url set");
+    }
+
+    info("Clicking the 'go back' button");
+    oncePanelUpdated = promiseAbuseReportUpdated(abuseReportEl, "reasons");
+    EventUtils.synthesizeMouseAtCenter(
+      _btnGoBack, {}, abuseReportEl.ownerGlobal);
+    await oncePanelUpdated;
+    ok(!_reasonsPanel.hidden, "Reasons panel should be visible");
+    ok(_submitPanel.hidden, "Submit panel should be hidden");
+  }
+
+  await closeAboutAddons();
+}
+
+add_task(async function test_abuse_report_suggestions_extension() {
+  const EXT_ID = "test-extension-suggestions@mochi.test";
+  const extension = await installTestExtension(EXT_ID);
+  await test_abuse_report_suggestions(EXT_ID);
+  await extension.unload();
+});
+
+add_task(async function test_abuse_report_suggestions_theme() {
+  const THEME_ID = "theme@mochi.test";
+  const theme = await installTestExtension(THEME_ID, "theme");
+  await test_abuse_report_suggestions(THEME_ID);
+  await theme.unload();
+});
+
+// This test case verifies the message bars created on other
+// scenarios (e.g. report creation and submissions errors).
+add_task(async function test_abuse_report_message_bars() {
+  const EXT_ID = "test-extension-errors@mochi.test";
+  const EXT_ID2 = "test-extension-errors-2@mochi.test";
+  const extension = await installTestExtension(EXT_ID);
+  const extension2 = await installTestExtension(EXT_ID2);
+
+  async function assertMessageBars(expectedMessageBarIds, test) {
+    await openAboutAddons();
+    const expectedLength = expectedMessageBarIds.length;
+    const onMessageBarsCreated = promiseMessageBars(expectedLength);
+    // Reset the timestamp of the last report between tests.
+    AbuseReporter._lastReportTimestamp = null;
+    const cleanup = await test();
+    info(`Waiting for ${expectedLength} message-bars to be created`);
+    const barDetails = await onMessageBarsCreated;
+    Assert.deepEqual(barDetails.map(d => d.definitionId), expectedMessageBarIds,
+                     "Got the expected message bars");
+    if (cleanup) {
+      await cleanup();
+    }
+    await closeAboutAddons();
+  }
+
+  function setTestRequestHandler(responseStatus, responseData) {
+    apiRequestHandler = ({request, response}) => {
+      response.setStatusLine(request.httpVersion, responseStatus, "Error");
+      response.write(responseData);
+    };
+  }
+
+  await assertMessageBars(["ERROR_ADDON_NOTFOUND"], async () => {
+    info("Test message bars on addon not found");
+    triggerNewAbuseReport(
+      "non-existend-addon-id@mochi.test", REPORT_ENTRY_POINT);
+  });
+
+  await assertMessageBars(["submitting", "ERROR_RECENT_SUBMIT"], async () => {
+    info("Test message bars on recent submission");
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    AbuseReporter.updateLastReportTimestamp();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "ERROR_ABORTED_SUBMIT"], async () => {
+    info("Test message bars on aborted submission");
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    const {report}  = getAbuseReportFrame();
+    report.abort();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "ERROR_SERVER"], async () => {
+    info("Test message bars on server error");
+    setTestRequestHandler(500);
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "ERROR_CLIENT"], async () => {
+    info("Test message bars on client error");
+    setTestRequestHandler(400);
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => {
+    info("Test message bars on unexpected status code");
+    setTestRequestHandler(604);
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => {
+    info("Test message bars on invalid json in the response data");
+    setTestRequestHandler(200, "");
+    triggerNewAbuseReport(EXT_ID, REPORT_ENTRY_POINT);
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  // Verify the 3 expected entry points:
+  //   menu, toolbar_context_menu and uninstall
+  // (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html).
+  await assertMessageBars(["submitting", "submitted"], async () => {
+    info("Test message bars on report opened from addon options menu");
+    setTestRequestHandler(200, "{}");
+    triggerNewAbuseReport(EXT_ID, "menu");
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "submitted"], async () => {
+    info("Test message bars on report opened from browserAction context menu");
+    setTestRequestHandler(200, "{}");
+    triggerNewAbuseReport(EXT_ID, "toolbar_context_menu");
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await assertMessageBars(["submitting", "submitted-and-removed"], async () => {
+    info("Test message bars on report opened from addon removal");
+    setTestRequestHandler(200, "{}");
+    triggerNewAbuseReport(EXT_ID2, "uninstall");
+    await promiseAbuseReportRendered();
+    triggerSubmitAbuseReport("fake-reason", "fake-message");
+  });
+
+  await extension.unload();
+  await extension2.unload();
+});