summaryrefslogtreecommitdiffstats
path: root/browser/components/reportbrokensite
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/reportbrokensite
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/reportbrokensite')
-rw-r--r--browser/components/reportbrokensite/ReportBrokenSite.sys.mjs770
-rw-r--r--browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml128
-rw-r--r--browser/components/reportbrokensite/moz.build14
-rw-r--r--browser/components/reportbrokensite/test/browser/browser.toml39
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js113
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_back_buttons.js37
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_error_messages.js64
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js113
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_parent_menuitems.js107
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js62
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js161
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_report_send.js79
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js89
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_send_more_info.js68
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_site_not_working_fallback.js35
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_tab_key_order.js133
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_tab_switch_handling.js81
-rw-r--r--browser/components/reportbrokensite/test/browser/example_report_page.html22
-rw-r--r--browser/components/reportbrokensite/test/browser/head.js863
-rw-r--r--browser/components/reportbrokensite/test/browser/send.js290
-rw-r--r--browser/components/reportbrokensite/test/browser/sendMoreInfoTestEndpoint.html27
-rw-r--r--browser/components/reportbrokensite/test/browser/send_more_info.js215
22 files changed, 3510 insertions, 0 deletions
diff --git a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs
new file mode 100644
index 0000000000..706c013383
--- /dev/null
+++ b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs
@@ -0,0 +1,770 @@
+/* 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-env mozilla/browser-window */
+
+const DEFAULT_NEW_REPORT_ENDPOINT = "https://webcompat.com/issues/new";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
+});
+
+const gDescriptionCheckRE = /\S/;
+
+class ViewState {
+ #doc;
+ #mainView;
+ #reportSentView;
+ #formElement;
+ #reasonOptions;
+ #randomizeReasons = false;
+
+ currentTabURI;
+ currentTabWebcompatDetailsPromise;
+
+ constructor(doc) {
+ this.#doc = doc;
+ this.#mainView = doc.ownerGlobal.PanelMultiView.getViewNode(
+ this.#doc,
+ "report-broken-site-popup-mainView"
+ );
+ this.#reportSentView = doc.ownerGlobal.PanelMultiView.getViewNode(
+ this.#doc,
+ "report-broken-site-popup-reportSentView"
+ );
+ this.#formElement = doc.ownerGlobal.PanelMultiView.getViewNode(
+ this.#doc,
+ "report-broken-site-panel-form"
+ );
+ ViewState.#cache.set(doc, this);
+
+ this.#reasonOptions = Array.from(
+ // Skip the first option ("choose reason"), since it always stays at the top
+ this.reasonInput.querySelectorAll(`option:not(:first-of-type)`)
+ );
+ }
+
+ static #cache = new WeakMap();
+ static get(doc) {
+ return ViewState.#cache.get(doc) ?? new ViewState(doc);
+ }
+
+ get mainPanelview() {
+ return this.#mainView;
+ }
+
+ get reportSentPanelview() {
+ return this.#reportSentView;
+ }
+
+ get urlInput() {
+ return this.#mainView.querySelector("#report-broken-site-popup-url");
+ }
+
+ get url() {
+ return this.urlInput.value;
+ }
+
+ set url(spec) {
+ this.urlInput.value = spec;
+ }
+
+ resetURLToCurrentTab() {
+ const { currentURI } = this.#doc.ownerGlobal.gBrowser.selectedBrowser;
+ this.currentTabURI = currentURI;
+ this.urlInput.value = currentURI.spec;
+ }
+
+ get descriptionInput() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-description"
+ );
+ }
+
+ get description() {
+ return this.descriptionInput.value;
+ }
+
+ set description(value) {
+ this.descriptionInput.value = value;
+ }
+
+ static REASON_CHOICES_ID_PREFIX = "report-broken-site-popup-reason-";
+
+ get reasonInput() {
+ return this.#mainView.querySelector("#report-broken-site-popup-reason");
+ }
+
+ get reason() {
+ const reason = this.reasonInput.selectedOptions[0].id.replace(
+ ViewState.REASON_CHOICES_ID_PREFIX,
+ ""
+ );
+ return reason == "choose" ? undefined : reason;
+ }
+
+ set reason(value) {
+ this.reasonInput.selectedIndex = this.#mainView.querySelector(
+ `#${ViewState.REASON_CHOICES_ID_PREFIX}${value}`
+ ).index;
+ }
+
+ #randomizeReasonsOrdering() {
+ // As with QuickActionsLoaderDefault, we use the Normandy
+ // randomizationId as our PRNG seed to ensure that the same
+ // user should always get the same sequence.
+ const seed = [...lazy.ClientEnvironment.randomizationId]
+ .map(x => x.charCodeAt(0))
+ .reduce((sum, a) => sum + a, 0);
+
+ const items = [...this.#reasonOptions];
+ this.#shuffleArray(items, seed);
+ items[0].parentNode.append(...items);
+ }
+
+ #shuffleArray(array, seed) {
+ // We use SplitMix as it is reputed to have a strong distribution of values.
+ const prng = this.#getSplitMix32PRNG(seed);
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(prng() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ }
+
+ // SplitMix32 is a splittable pseudorandom number generator (PRNG).
+ // License: MIT (https://github.com/attilabuti/SimplexNoise)
+ #getSplitMix32PRNG(a) {
+ return () => {
+ a |= 0;
+ a = (a + 0x9e3779b9) | 0;
+ var t = a ^ (a >>> 16);
+ t = Math.imul(t, 0x21f0aaad);
+ t = t ^ (t >>> 15);
+ t = Math.imul(t, 0x735a2d97);
+ return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
+ };
+ }
+
+ #restoreReasonsOrdering() {
+ this.#reasonOptions[0].parentNode.append(...this.#reasonOptions);
+ }
+
+ get form() {
+ return this.#formElement;
+ }
+
+ reset() {
+ this.currentTabWebcompatDetailsPromise = undefined;
+ this.form.reset();
+
+ this.resetURLToCurrentTab();
+ }
+
+ ensureReasonOrderingMatchesPref() {
+ const randomizeReasons =
+ this.#doc.ownerGlobal.ReportBrokenSite.randomizeReasons;
+ if (randomizeReasons != this.#randomizeReasons) {
+ if (randomizeReasons) {
+ this.#randomizeReasonsOrdering();
+ } else {
+ this.#restoreReasonsOrdering();
+ }
+ this.#randomizeReasons = randomizeReasons;
+ }
+ }
+
+ get isURLValid() {
+ return this.urlInput.checkValidity();
+ }
+
+ get isReasonValid() {
+ const { reasonEnabled, reasonIsOptional } =
+ this.#doc.ownerGlobal.ReportBrokenSite;
+ return (
+ !reasonEnabled || reasonIsOptional || this.reasonInput.checkValidity()
+ );
+ }
+
+ get isDescriptionValid() {
+ const { descriptionIsOptional } = this.#doc.ownerGlobal.ReportBrokenSite;
+ return (
+ descriptionIsOptional ||
+ gDescriptionCheckRE.test(this.descriptionInput.value)
+ );
+ }
+
+ #focusMainViewElement(toFocus) {
+ const panelview = this.#doc.ownerGlobal.PanelView.forNode(this.#mainView);
+ panelview.selectedElement = toFocus;
+ panelview.focusSelectedElement();
+ }
+
+ focusFirstInvalidElement() {
+ if (!this.isURLValid) {
+ this.#focusMainViewElement(this.urlInput);
+ } else if (!this.isReasonValid) {
+ this.#focusMainViewElement(this.reasonInput);
+ this.reasonInput.showPicker();
+ } else if (!this.isDescriptionValid) {
+ this.#focusMainViewElement(this.descriptionInput);
+ }
+ }
+
+ get sendMoreInfoLink() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-send-more-info-link"
+ );
+ }
+
+ get reasonLabelRequired() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-reason-label"
+ );
+ }
+
+ get reasonLabelOptional() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-reason-optional-label"
+ );
+ }
+
+ get descriptionLabelRequired() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-description-label"
+ );
+ }
+
+ get descriptionLabelOptional() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-description-optional-label"
+ );
+ }
+
+ get sendButton() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-send-button"
+ );
+ }
+
+ get cancelButton() {
+ return this.#mainView.querySelector(
+ "#report-broken-site-popup-cancel-button"
+ );
+ }
+
+ get mainView() {
+ return this.#mainView;
+ }
+
+ get reportSentView() {
+ return this.#reportSentView;
+ }
+
+ get okayButton() {
+ return this.#reportSentView.querySelector(
+ "#report-broken-site-popup-okay-button"
+ );
+ }
+}
+
+export var ReportBrokenSite = new (class ReportBrokenSite {
+ #newReportEndpoint = undefined;
+
+ get sendMoreInfoEndpoint() {
+ return this.#newReportEndpoint || DEFAULT_NEW_REPORT_ENDPOINT;
+ }
+
+ static WEBCOMPAT_REPORTER_CONFIG = {
+ src: "desktop-reporter",
+ utm_campaign: "report-broken-site",
+ utm_source: "desktop-reporter",
+ };
+
+ static DATAREPORTING_PREF = "datareporting.healthreport.uploadEnabled";
+ static REPORTER_ENABLED_PREF = "ui.new-webcompat-reporter.enabled";
+
+ static REASON_PREF = "ui.new-webcompat-reporter.reason-dropdown";
+ static REASON_PREF_VALUES = {
+ 0: "disabled",
+ 1: "optional",
+ 2: "required",
+ };
+ static REASON_RANDOMIZED_PREF =
+ "ui.new-webcompat-reporter.reason-dropdown.randomized";
+ static SEND_MORE_INFO_PREF = "ui.new-webcompat-reporter.send-more-info-link";
+ static NEW_REPORT_ENDPOINT_PREF =
+ "ui.new-webcompat-reporter.new-report-endpoint";
+ static REPORT_SITE_ISSUE_PREF = "extensions.webcompat-reporter.enabled";
+
+ static MAIN_PANELVIEW_ID = "report-broken-site-popup-mainView";
+ static SENT_PANELVIEW_ID = "report-broken-site-popup-reportSentView";
+
+ #_enabled = false;
+ get enabled() {
+ return this.#_enabled;
+ }
+
+ #reasonEnabled = false;
+ #reasonIsOptional = true;
+ #randomizeReasons = false;
+ #descriptionIsOptional = true;
+ #sendMoreInfoEnabled = true;
+
+ get reasonEnabled() {
+ return this.#reasonEnabled;
+ }
+
+ get reasonIsOptional() {
+ return this.#reasonIsOptional;
+ }
+
+ get randomizeReasons() {
+ return this.#randomizeReasons;
+ }
+
+ get descriptionIsOptional() {
+ return this.#descriptionIsOptional;
+ }
+
+ constructor() {
+ for (const [name, [pref, dflt]] of Object.entries({
+ dataReportingPref: [ReportBrokenSite.DATAREPORTING_PREF, false],
+ reasonPref: [ReportBrokenSite.REASON_PREF, 0],
+ reasonRandomizedPref: [ReportBrokenSite.REASON_RANDOMIZED_PREF, false],
+ sendMoreInfoPref: [ReportBrokenSite.SEND_MORE_INFO_PREF, false],
+ newReportEndpointPref: [
+ ReportBrokenSite.NEW_REPORT_ENDPOINT_PREF,
+ DEFAULT_NEW_REPORT_ENDPOINT,
+ ],
+ enabledPref: [ReportBrokenSite.REPORTER_ENABLED_PREF, true],
+ reportSiteIssueEnabledPref: [
+ ReportBrokenSite.REPORT_SITE_ISSUE_PREF,
+ false,
+ ],
+ })) {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ name,
+ pref,
+ dflt,
+ this.#checkPrefs.bind(this)
+ );
+ }
+ this.#checkPrefs();
+ }
+
+ canReportURI(uri) {
+ return uri && (uri.schemeIs("http") || uri.schemeIs("https"));
+ }
+
+ #recordGleanEvent(name, extra) {
+ Glean.webcompatreporting[name].record(extra);
+ }
+
+ updateParentMenu(event) {
+ // We need to make sure that the Report Broken Site menu item
+ // is disabled and/or hidden depending on the prefs/active tab URL
+ // when our parent popups are shown, and if their tab's location
+ // changes while they are open.
+ const tabbrowser = event.target.ownerGlobal.gBrowser;
+ this.enableOrDisableMenuitems(tabbrowser.selectedBrowser);
+
+ tabbrowser.addTabsProgressListener(this);
+ event.target.addEventListener(
+ "popuphidden",
+ () => {
+ tabbrowser.removeTabsProgressListener(this);
+ },
+ { once: true }
+ );
+ }
+
+ init(tabbrowser) {
+ // Called in browser.js.
+ const { ownerGlobal } = tabbrowser.selectedBrowser;
+ const { document } = ownerGlobal;
+
+ const state = ViewState.get(document);
+
+ this.#initMainView(state);
+ this.#initReportSentView(state);
+
+ for (const id of ["menu_HelpPopup", "appMenu-popup"]) {
+ document
+ .getElementById(id)
+ .addEventListener("popupshown", this.updateParentMenu.bind(this));
+ }
+
+ state.mainPanelview.addEventListener("ViewShowing", ({ target }) => {
+ const { selectedBrowser } = target.ownerGlobal.gBrowser;
+ let source = "helpMenu";
+ switch (target.closest("panelmultiview")?.id) {
+ case "appMenu-multiView":
+ source = "hamburgerMenu";
+ break;
+ case "protections-popup-multiView":
+ source = "ETPShieldIconMenu";
+ break;
+ }
+ this.#onMainViewShown(source, selectedBrowser);
+ });
+
+ // Make sure the URL input is focused when the main view pops up.
+ state.mainPanelview.addEventListener("ViewShown", () => {
+ const panelview = ownerGlobal.PanelView.forNode(state.mainPanelview);
+ panelview.selectedElement = state.urlInput;
+ panelview.focusSelectedElement();
+ });
+
+ // Make sure the Okay button is focused when the report sent view pops up.
+ state.reportSentPanelview.addEventListener("ViewShown", () => {
+ const panelview = ownerGlobal.PanelView.forNode(
+ state.reportSentPanelview
+ );
+ panelview.selectedElement = state.okayButton;
+ panelview.focusSelectedElement();
+ });
+ }
+
+ enableOrDisableMenuitems(selectedbrowser) {
+ // Ensures that the various Report Broken Site menu items and
+ // toolbar buttons are enabled/hidden when appropriate (and
+ // also the Help menu's Report Site Issue item)/
+
+ const canReportUrl = this.canReportURI(selectedbrowser.currentURI);
+
+ const { document } = selectedbrowser.ownerGlobal;
+
+ const cmd = document.getElementById("cmd_reportBrokenSite");
+ if (this.enabled) {
+ cmd.setAttribute("hidden", "false"); // see bug 805653
+ } else {
+ cmd.setAttribute("hidden", "true");
+ }
+ if (canReportUrl) {
+ cmd.removeAttribute("disabled");
+ } else {
+ cmd.setAttribute("disabled", "true");
+ }
+
+ // Changes to the "hidden" and "disabled" state of the command aren't reliably
+ // reflected on the main menu unless we open it twice, or do it manually.
+ // (See bug 1864953).
+ const mainmenuItem = document.getElementById("help_reportBrokenSite");
+ if (mainmenuItem) {
+ mainmenuItem.hidden = !this.enabled;
+ mainmenuItem.disabled = !canReportUrl;
+ }
+
+ // Report Site Issue is our older issue reporter, shown in the Help
+ // menu on pre-release channels. We should hide it unless we're
+ // disabled, at which point we should show it when available.
+ const reportSiteIssue = document.getElementById("help_reportSiteIssue");
+ if (reportSiteIssue) {
+ reportSiteIssue.hidden = this.enabled || !this.reportSiteIssueEnabledPref;
+ reportSiteIssue.disabled = !canReportUrl;
+ }
+
+ // "Site not working?" on the protections panel should be hidden when
+ // Report Broken Site is visible (bug 1868527).
+ const siteNotWorking = document.getElementById(
+ "protections-popup-tp-switch-section-footer"
+ );
+ if (siteNotWorking) {
+ siteNotWorking.hidden = this.enabled;
+ }
+ }
+
+ #checkPrefs(whichChanged) {
+ // No breakage reports can be sent by Glean if it's disabled, so we also
+ // disable the broken site reporter. We also have our own pref.
+ this.#_enabled =
+ Services.policies.isAllowed("feedbackCommands") &&
+ this.dataReportingPref &&
+ this.enabledPref;
+
+ this.#reasonEnabled = this.reasonPref == 1 || this.reasonPref == 2;
+ this.#reasonIsOptional = this.reasonPref == 1;
+ if (!whichChanged || whichChanged == ReportBrokenSite.REASON_PREF) {
+ const setting = ReportBrokenSite.REASON_PREF_VALUES[this.reasonPref];
+ this.#recordGleanEvent("reasonDropdown", { setting });
+ }
+
+ this.#sendMoreInfoEnabled = this.sendMoreInfoPref;
+ this.#newReportEndpoint = this.newReportEndpointPref;
+
+ this.#randomizeReasons = this.reasonRandomizedPref;
+ }
+
+ #initMainView(state) {
+ state.sendButton.addEventListener("command", () => {
+ state.form.requestSubmit();
+ });
+
+ state.form.addEventListener("submit", async event => {
+ event.preventDefault();
+ if (!state.form.checkValidity()) {
+ state.focusFirstInvalidElement();
+ return;
+ }
+ const multiview = event.target.closest("panelmultiview");
+ this.#recordGleanEvent("send");
+ await this.#sendReportAsGleanPing(state);
+ multiview.showSubView("report-broken-site-popup-reportSentView");
+ state.reset();
+ });
+
+ state.cancelButton.addEventListener("command", ({ target }) => {
+ target.ownerGlobal.CustomizableUI.hidePanelForNode(target);
+ state.reset();
+ });
+
+ state.sendMoreInfoLink.addEventListener("click", async event => {
+ event.preventDefault();
+ const tabbrowser = event.target.ownerGlobal.gBrowser;
+ this.#recordGleanEvent("sendMoreInfo");
+ event.target.ownerGlobal.CustomizableUI.hidePanelForNode(event.target);
+ await this.#openWebCompatTab(tabbrowser);
+ state.reset();
+ });
+ }
+
+ #initReportSentView(state) {
+ state.okayButton.addEventListener("command", ({ target }) => {
+ target.ownerGlobal.CustomizableUI.hidePanelForNode(target);
+ });
+ }
+
+ async #onMainViewShown(source, selectedBrowser) {
+ const { document } = selectedBrowser.ownerGlobal;
+
+ let didReset = false;
+ const state = ViewState.get(document);
+ const uri = selectedBrowser.currentURI;
+ if (!state.isURLValid && !state.isDescriptionValid) {
+ state.reset();
+ didReset = true;
+ } else if (!state.currentTabURI || !uri.equals(state.currentTabURI)) {
+ state.reset();
+ didReset = true;
+ } else if (!state.url) {
+ state.resetURLToCurrentTab();
+ }
+
+ const { sendMoreInfoLink } = state;
+ const { sendMoreInfoEndpoint } = this;
+ if (sendMoreInfoLink.href !== sendMoreInfoEndpoint) {
+ sendMoreInfoLink.href = sendMoreInfoEndpoint;
+ }
+ sendMoreInfoLink.hidden = !this.#sendMoreInfoEnabled;
+
+ state.reasonInput.hidden = !this.#reasonEnabled;
+ state.reasonInput.required = this.#reasonEnabled && !this.#reasonIsOptional;
+
+ state.ensureReasonOrderingMatchesPref();
+
+ state.reasonLabelRequired.hidden =
+ !this.#reasonEnabled || this.#reasonIsOptional;
+ state.reasonLabelOptional.hidden =
+ !this.#reasonEnabled || !this.#reasonIsOptional;
+
+ state.descriptionLabelRequired.hidden = this.#descriptionIsOptional;
+ state.descriptionLabelOptional.hidden = !this.#descriptionIsOptional;
+
+ this.#recordGleanEvent("opened", { source });
+
+ if (didReset || !state.currentTabWebcompatDetailsPromise) {
+ state.currentTabWebcompatDetailsPromise = this.#queryActor(
+ "GetWebCompatInfo",
+ undefined,
+ selectedBrowser
+ ).catch(err => {
+ console.error("Report Broken Site: unexpected error", err);
+ });
+ }
+ }
+
+ async #queryActor(msg, params, browser) {
+ const actor =
+ browser.browsingContext.currentWindowGlobal.getActor("ReportBrokenSite");
+ return actor.sendQuery(msg, params);
+ }
+
+ async #loadTab(tabbrowser, url, triggeringPrincipal) {
+ const tab = tabbrowser.addTab(url, {
+ inBackground: false,
+ triggeringPrincipal,
+ });
+ const expectedBrowser = tabbrowser.getBrowserForTab(tab);
+ return new Promise(resolve => {
+ const listener = {
+ onLocationChange(browser, webProgress, request, uri, flags) {
+ if (
+ browser == expectedBrowser &&
+ uri.spec == url &&
+ webProgress.isTopLevel
+ ) {
+ resolve(tab);
+ tabbrowser.removeTabsProgressListener(listener);
+ }
+ },
+ };
+ tabbrowser.addTabsProgressListener(listener);
+ });
+ }
+
+ async #openWebCompatTab(tabbrowser) {
+ const endpointUrl = this.sendMoreInfoEndpoint;
+ const principal = Services.scriptSecurityManager.createNullPrincipal({});
+ const tab = await this.#loadTab(tabbrowser, endpointUrl, principal);
+ const { document } = tabbrowser.selectedBrowser.ownerGlobal;
+ const { description, reason, url, currentTabWebcompatDetailsPromise } =
+ ViewState.get(document);
+
+ return this.#queryActor(
+ "SendDataToWebcompatCom",
+ {
+ reason,
+ description,
+ endpointUrl,
+ reportUrl: url,
+ reporterConfig: ReportBrokenSite.WEBCOMPAT_REPORTER_CONFIG,
+ webcompatInfo: await currentTabWebcompatDetailsPromise,
+ },
+ tab.linkedBrowser
+ ).catch(err => {
+ console.error("Report Broken Site: unexpected error", err);
+ });
+ }
+
+ async #sendReportAsGleanPing({
+ currentTabWebcompatDetailsPromise,
+ description,
+ reason,
+ url,
+ }) {
+ const gBase = Glean.brokenSiteReport;
+ const gTabInfo = Glean.brokenSiteReportTabInfo;
+ const gAntitracking = Glean.brokenSiteReportTabInfoAntitracking;
+ const gFrameworks = Glean.brokenSiteReportTabInfoFrameworks;
+ const gApp = Glean.brokenSiteReportBrowserInfoApp;
+ const gGraphics = Glean.brokenSiteReportBrowserInfoGraphics;
+ const gPrefs = Glean.brokenSiteReportBrowserInfoPrefs;
+ const gSystem = Glean.brokenSiteReportBrowserInfoSystem;
+
+ if (reason) {
+ gBase.breakageCategory.set(reason);
+ }
+
+ gBase.description.set(description);
+ gBase.url.set(url);
+
+ const details = await currentTabWebcompatDetailsPromise;
+
+ if (!details) {
+ GleanPings.brokenSiteReport.submit();
+ return;
+ }
+
+ const {
+ antitracking,
+ browser,
+ devicePixelRatio,
+ frameworks,
+ languages,
+ userAgent,
+ } = details;
+
+ gTabInfo.languages.set(languages);
+ gTabInfo.useragentString.set(userAgent);
+ gGraphics.devicePixelRatio.set(devicePixelRatio);
+
+ for (const [name, value] of Object.entries(antitracking)) {
+ gAntitracking[name].set(value);
+ }
+
+ for (const [name, value] of Object.entries(frameworks)) {
+ gFrameworks[name].set(value);
+ }
+
+ const { app, graphics, locales, platform, prefs, security } = browser;
+
+ gApp.defaultLocales.set(locales);
+ gApp.defaultUseragentString.set(app.defaultUserAgent);
+
+ const { fissionEnabled, isTablet, memoryMB } = platform;
+ gApp.fissionEnabled.set(fissionEnabled);
+ gSystem.isTablet.set(isTablet ?? false);
+ gSystem.memory.set(memoryMB);
+
+ gPrefs.cookieBehavior.set(prefs["network.cookie.cookieBehavior"]);
+ gPrefs.forcedAcceleratedLayers.set(
+ prefs["layers.acceleration.force-enabled"]
+ );
+ gPrefs.globalPrivacyControlEnabled.set(
+ prefs["privacy.globalprivacycontrol.enabled"]
+ );
+ gPrefs.installtriggerEnabled.set(
+ prefs["extensions.InstallTrigger.enabled"]
+ );
+ gPrefs.opaqueResponseBlocking.set(prefs["browser.opaqueResponseBlocking"]);
+ gPrefs.resistFingerprintingEnabled.set(
+ prefs["privacy.resistFingerprinting"]
+ );
+ gPrefs.softwareWebrender.set(prefs["gfx.webrender.software"]);
+
+ if (security) {
+ for (const [name, value] of Object.entries(security)) {
+ if (value?.length) {
+ Glean.brokenSiteReportBrowserInfoSecurity[name].set(value);
+ }
+ }
+ }
+
+ const { devices, drivers, features, hasTouchScreen, monitors } = graphics;
+
+ gGraphics.devicesJson.set(JSON.stringify(devices));
+ gGraphics.driversJson.set(JSON.stringify(drivers));
+ gGraphics.featuresJson.set(JSON.stringify(features));
+ gGraphics.hasTouchScreen.set(hasTouchScreen);
+ gGraphics.monitorsJson.set(JSON.stringify(monitors));
+
+ GleanPings.brokenSiteReport.submit();
+ }
+
+ open(event) {
+ const { target } = event.sourceEvent;
+ const { selectedBrowser } = target.ownerGlobal.gBrowser;
+ const { ownerGlobal } = selectedBrowser;
+ const { document } = ownerGlobal;
+
+ switch (target.id) {
+ case "appMenu-report-broken-site-button":
+ ownerGlobal.PanelUI.showSubView(
+ ReportBrokenSite.MAIN_PANELVIEW_ID,
+ target
+ );
+ break;
+ case "protections-popup-report-broken-site-button":
+ document
+ .getElementById("protections-popup-multiView")
+ .showSubView(ReportBrokenSite.MAIN_PANELVIEW_ID);
+ break;
+ case "help_reportBrokenSite":
+ // hide the hamburger menu first, as we overlap with it.
+ const appMenuPopup = document.getElementById("appMenu-popup");
+ appMenuPopup?.hidePopup();
+
+ ownerGlobal.PanelUI.showSubView(
+ ReportBrokenSite.MAIN_PANELVIEW_ID,
+ ownerGlobal.PanelUI.menuButton
+ );
+ break;
+ }
+ }
+})();
diff --git a/browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml b/browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml
new file mode 100644
index 0000000000..73132d5f37
--- /dev/null
+++ b/browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml
@@ -0,0 +1,128 @@
+<!-- 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/. -->
+
+<panelview id="report-broken-site-popup-mainView"
+ class="report-broken-site-view main-view PanelUI-subView"
+ role="dialog"
+ aria-describedby="report-broken-site-panel-intro"
+ data-l10n-id="report-broken-site-panel-header"
+ data-l10n-attrs="title"
+ mainview-with-header="true">
+ <!-- The panel can be opened from the Help menubar, so with the current
+ - headerText implementation in PanelMultiView.sys.mjs the markup must
+ - be added manually. -->
+ <box class="panel-header">
+ <html:h1>
+ <html:span data-l10n-id="report-broken-site-mainview-title"/>
+ </html:h1>
+ </box>
+ <toolbarseparator/>
+ <!-- This form is used purely in JS for validation. We hide it here
+ rather than nesting it in the vbox to keep our CSS simpler. -->
+ <html:form id="report-broken-site-panel-form"/>
+ <vbox id="report-broken-site-panel-container"
+ class="panel-subview-body">
+ <html:p id="report-broken-site-panel-intro"
+ data-l10n-id="report-broken-site-panel-intro"/>
+ <label id="report-broken-site-popup-url-label"
+ control="report-broken-site-popup-url"
+ data-l10n-id="report-broken-site-panel-url"/>
+ <html:input id="report-broken-site-popup-url"
+ form="report-broken-site-panel-form"
+ allow-arrow-navigation="true"
+ type="url" required="required" aria-required="true" />
+ <label id="report-broken-site-popup-invalid-url-msg"
+ class="invalid-message text-error"
+ role="alert"
+ data-l10n-id="report-broken-site-panel-invalid-url-label"/>
+ <label id="report-broken-site-popup-reason-label"
+ control="report-broken-site-popup-reason"
+ hidden="true"
+ data-l10n-attrs="aria-label"
+ data-l10n-id="report-broken-site-panel-reason-label"/>
+ <label id="report-broken-site-popup-reason-optional-label"
+ control="report-broken-site-popup-reason"
+ hidden="true"
+ data-l10n-attrs="aria-label"
+ data-l10n-id="report-broken-site-panel-reason-optional-label"/>
+ <html:select id="report-broken-site-popup-reason"
+ form="report-broken-site-panel-form">
+ <html:option id="report-broken-site-popup-reason-choose"
+ value=""
+ hidden="true"
+ disabled="true"
+ selected="selected"
+ data-l10n-id="report-broken-site-panel-reason-choose"/>
+ <html:option id="report-broken-site-popup-reason-slow"
+ data-l10n-id="report-broken-site-panel-reason-slow"/>
+ <html:option id="report-broken-site-popup-reason-media"
+ data-l10n-id="report-broken-site-panel-reason-media"/>
+ <html:option id="report-broken-site-popup-reason-content"
+ data-l10n-id="report-broken-site-panel-reason-content"/>
+ <html:option id="report-broken-site-popup-reason-account"
+ data-l10n-id="report-broken-site-panel-reason-account"/>
+ <html:option id="report-broken-site-popup-reason-adblockers"
+ data-l10n-id="report-broken-site-panel-reason-adblockers"/>
+ <html:option id="report-broken-site-popup-reason-other"
+ data-l10n-id="report-broken-site-panel-reason-other"/>
+ </html:select>
+ <label id="report-broken-site-popup-missing-reason-msg"
+ class="invalid-message text-error"
+ role="alert"
+ data-l10n-id="report-broken-site-panel-missing-reason-label"/>
+ <label id="report-broken-site-popup-description-label"
+ control="report-broken-site-popup-description"
+ hidden="true"
+ data-l10n-id="report-broken-site-panel-description-label"/>
+ <label id="report-broken-site-popup-description-optional-label"
+ control="report-broken-site-popup-description"
+ hidden="true"
+ data-l10n-id="report-broken-site-panel-description-optional-label"/>
+ <html:textarea id="report-broken-site-popup-description"
+ form="report-broken-site-panel-form"
+ allow-arrow-navigation="true"
+ rows="8"></html:textarea>
+ <hbox>
+ <html:a id="report-broken-site-popup-send-more-info-link"
+ href="#"
+ data-l10n-id="report-broken-site-panel-send-more-info-link"/>
+ </hbox>
+ </vbox>
+ <html:moz-button-group class="panel-footer">
+ <button id="report-broken-site-popup-cancel-button"
+ class="footer-button"
+ data-l10n-id="report-broken-site-panel-button-cancel"/>
+ <button id="report-broken-site-popup-send-button"
+ class="footer-button"
+ default="true"
+ data-l10n-id="report-broken-site-panel-button-send"/>
+ </html:moz-button-group>
+</panelview>
+
+<panelview id="report-broken-site-popup-reportSentView"
+ class="report-broken-site-view sent-view PanelUI-subView"
+ role="dialog"
+ data-l10n-id="report-broken-site-panel-report-sent-header"
+ data-l10n-attrs="title"
+ aria-describedby="report-broken-site-panel-report-sent-text"
+ has-customized-header="true"
+ no-back-button="true">
+ <box class="panel-header panel-header-with-additional-element">
+ <image class="checkmark-icon additional-element-on-inline-start"></image>
+ <html:h1>
+ <html:span data-l10n-id="report-broken-site-panel-report-sent-label"/>
+ </html:h1>
+ </box>
+ <vbox class="panel-subview-body">
+ <html:p id="report-broken-site-panel-report-sent-text"
+ data-l10n-id="report-broken-site-panel-report-sent-text"
+ class="subview-subheader"/>
+ </vbox>
+ <html:moz-button-group class="panel-footer">
+ <button id="report-broken-site-popup-okay-button"
+ class="footer-button"
+ default="true"
+ data-l10n-id="report-broken-site-panel-button-okay"/>
+ </html:moz-button-group>
+</panelview>
diff --git a/browser/components/reportbrokensite/moz.build b/browser/components/reportbrokensite/moz.build
new file mode 100644
index 0000000000..ad9b8b722e
--- /dev/null
+++ b/browser/components/reportbrokensite/moz.build
@@ -0,0 +1,14 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+EXTRA_JS_MODULES += [
+ "ReportBrokenSite.sys.mjs",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Web Compatibility", "Tooling & Investigations")
diff --git a/browser/components/reportbrokensite/test/browser/browser.toml b/browser/components/reportbrokensite/test/browser/browser.toml
new file mode 100644
index 0000000000..09e4b72079
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser.toml
@@ -0,0 +1,39 @@
+[DEFAULT]
+tags = "report-broken-site"
+support-files = [
+ "example_report_page.html",
+ "head.js",
+ "sendMoreInfoTestEndpoint.html",
+]
+
+["browser_antitracking_data_sent.js"]
+support-files = [ "send_more_info.js" ]
+
+["browser_back_buttons.js"]
+
+["browser_error_messages.js"]
+
+["browser_keyboard_navigation.js"]
+
+["browser_parent_menuitems.js"]
+
+["browser_prefers_contrast.js"]
+
+["browser_reason_dropdown.js"]
+
+["browser_report_send.js"]
+support-files = [ "send.js" ]
+
+["browser_report_site_issue_fallback.js"]
+
+["browser_send_more_info.js"]
+support-files = [
+ "send_more_info.js",
+ "../../../../../toolkit/components/gfx/content/videotest.mp4",
+]
+
+["browser_site_not_working_fallback.js"]
+
+["browser_tab_key_order.js"]
+
+["browser_tab_switch_handling.js"]
diff --git a/browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js b/browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js
new file mode 100644
index 0000000000..794fcd35a0
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the right data is sent for
+ * private windows and when ETP blocks content.
+ */
+
+/* import-globals-from send.js */
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+add_task(setupStrictETP);
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const blockedPromise = waitForContentBlockingEvent(4, win);
+ const tab = await openTab(REPORTABLE_PAGE_URL3, win);
+ await blockedPromise;
+
+ await testSend(tab, AppMenu(win), {
+ breakageCategory: "adblockers",
+ description: "another test description",
+ antitracking: {
+ blockList: "strict",
+ isPrivateBrowsing: true,
+ hasTrackingContentBlocked: true,
+ hasMixedActiveContentBlocked: true,
+ hasMixedDisplayContentBlocked: true,
+ },
+ frameworks: {
+ fastclick: true,
+ marfeel: true,
+ mobify: true,
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const blockedPromise = waitForContentBlockingEvent(4, win);
+ const tab = await openTab(REPORTABLE_PAGE_URL3, win);
+ await blockedPromise;
+
+ await testSendMoreInfo(tab, HelpMenu(win), {
+ antitracking: {
+ blockList: "strict",
+ isPrivateBrowsing: true,
+ hasTrackingContentBlocked: true,
+ hasMixedActiveContentBlocked: true,
+ hasMixedDisplayContentBlocked: true,
+ },
+ frameworks: { fastclick: true, mobify: true, marfeel: true },
+ consoleLog: [
+ {
+ level: "error",
+ log(actual) {
+ // "Blocked loading mixed display content http://example.com/tests/image/test/mochitest/blue.png"
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("blue.png")
+ );
+ },
+ pos: "0:1",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ {
+ level: "error",
+ log(actual) {
+ // "Blocked loading mixed active content http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html",
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("benignPage.html")
+ );
+ },
+ pos: "0:1",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ {
+ level: "warn",
+ log(actual) {
+ // "The resource at https://trackertest.org/ was blocked because content blocking is enabled.",
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("trackertest.org")
+ );
+ },
+ pos: "0:0",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ ],
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_back_buttons.js b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js
new file mode 100644
index 0000000000..b8de5f8e95
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that Report Broken Site popups will be
+ * reset to whichever tab the user is on as they change
+ * between windows and tabs. */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testBackButtonsAreAdded() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ let rbs = await AppMenu().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+
+ rbs = await ProtectionsPanel().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+ }
+ );
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_error_messages.js b/browser/components/reportbrokensite/test/browser/browser_error_messages.js
new file mode 100644
index 0000000000..54b93cb2da
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_error_messages.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the Report Broken Site errors messages are shown on
+ * the UI if the user enters an invalid URL or clicks the send
+ * button while it is disabled due to not selecting a "reason"
+ */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function test() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonRequired();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
+ const rbs = await menu.openReportBrokenSite();
+ const { sendButton, URLInput } = rbs;
+
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("https://asdf");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("http:/ /asdf");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("https://asdf");
+ const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
+ await selectPromise;
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageShown();
+ await rbs.dismissDropdownPopup();
+
+ rbs.chooseReason("slow");
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("");
+ rbs.chooseReason("choose");
+ window.ownerGlobal.document.activeElement?.blur();
+ const focusPromise = BrowserTestUtils.waitForEvent(URLInput, "focus");
+ EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
+ await focusPromise;
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageShown();
+
+ rbs.clickCancel();
+ }
+ });
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js
new file mode 100644
index 0000000000..4c37866628
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that sending or canceling reports with
+ * the Send and Cancel buttons work (as well as the Okay button)
+ */
+
+"use strict";
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+async function testPressingKey(key, tabToMatch, makePromise, followUp) {
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
+ info(
+ `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}`
+ );
+ const rbs = await menu.openReportBrokenSite();
+ const promise = makePromise(rbs);
+ if (tabToMatch) {
+ if (await tabTo(tabToMatch)) {
+ await pressKeyAndAwait(promise, key);
+ followUp && (await followUp(rbs));
+ await rbs.close();
+ ok(true, `was able to activate ${tabToMatch} with keyboard`);
+ } else {
+ await rbs.close();
+ ok(false, `could not tab to ${tabToMatch}`);
+ }
+ } else {
+ await pressKeyAndAwait(promise, key);
+ followUp && (await followUp(rbs));
+ await rbs.close();
+ ok(true, `was able to use keyboard`);
+ }
+ }
+ }
+ );
+}
+
+add_task(async function testSendMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-more-info-link",
+ rbs => rbs.waitForSendMoreInfoTab(),
+ () => gBrowser.removeCurrentTab()
+ );
+});
+
+add_task(async function testCancel() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-cancel-button",
+ rbs => BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
+ );
+});
+
+add_task(async function testSendAndOkay() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-button",
+ rbs => rbs.awaitReportSentViewOpened(),
+ async rbs => {
+ await tabTo("#report-broken-site-popup-okay-button");
+ const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
+ await pressKeyAndAwait(promise, "KEY_Enter");
+ }
+ );
+});
+
+add_task(async function testESCOnMain() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey("KEY_Escape", undefined, rbs =>
+ BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
+ );
+});
+
+add_task(async function testESCOnSent() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-button",
+ rbs => rbs.awaitReportSentViewOpened(),
+ async rbs => {
+ const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
+ await pressKeyAndAwait(promise, "KEY_Escape");
+ }
+ );
+});
+
+add_task(async function testBackButtons() {
+ ensureReportBrokenSitePreffedOn();
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ for (const menu of [AppMenu(), ProtectionsPanel()]) {
+ await menu.openReportBrokenSite();
+ await tabTo("#report-broken-site-popup-mainView .subviewbutton-back");
+ const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown");
+ await pressKeyAndAwait(promise, "KEY_Enter");
+ menu.close();
+ }
+ }
+ );
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_parent_menuitems.js b/browser/components/reportbrokensite/test/browser/browser_parent_menuitems.js
new file mode 100644
index 0000000000..11b2ebc5ec
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_parent_menuitems.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the Report Broken Site menu items are disabled
+ * when the active tab is not on a reportable URL, and is hidden
+ * when the feature is disabled via pref. Also ensure that the
+ * Report Broken Site item that is automatically generated in
+ * the app menu's help sub-menu is hidden.
+ */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testAppMenuHelpSubmenuItemIsHidden() {
+ ensureReportBrokenSitePreffedOn();
+ const menu = AppMenuHelpSubmenu();
+ await menu.open();
+ isMenuItemHidden(menu.reportBrokenSite);
+ await menu.close();
+
+ ensureReportBrokenSitePreffedOff();
+ await menu.open();
+ isMenuItemHidden(menu.reportBrokenSite);
+ await menu.close();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await menu.open();
+ isMenuItemHidden(menu.reportBrokenSite);
+ await menu.close();
+
+ ensureReportBrokenSitePreffedOn();
+ await menu.open();
+ isMenuItemHidden(menu.reportBrokenSite);
+ await menu.close();
+ });
+});
+
+add_task(async function testOtherMenus() {
+ ensureReportBrokenSitePreffedOff();
+
+ const appMenu = AppMenu();
+ const menus = [appMenu, ProtectionsPanel(), HelpMenu()];
+
+ async function forceMenuItemStateUpdate() {
+ window.ReportBrokenSite.enableOrDisableMenuitems(window);
+
+ // the hidden/disabled state of all of the menuitems may not update until one
+ // is rendered; then the related <command>'s state is propagated to them all.
+ await appMenu.open();
+ await appMenu.close();
+ }
+
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden on invalid page when preffed off`
+ );
+ }
+ });
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden on valid page when preffed off`
+ );
+ }
+ });
+
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemDisabled(
+ reportBrokenSite,
+ `${menuDescription} option disabled on invalid page when preffed on`
+ );
+ }
+ });
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemEnabled(
+ reportBrokenSite,
+ `${menuDescription} option enabled on valid page when preffed on`
+ );
+ }
+ });
+
+ ensureReportBrokenSitePreffedOff();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden again when pref toggled back off`
+ );
+ }
+ });
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js
new file mode 100644
index 0000000000..7097a662e5
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the background color of the "report sent"
+ * view is not green in non-default contrast modes.
+ */
+
+"use strict";
+
+add_common_setup();
+
+const HIGH_CONTRAST_MODE_OFF = [
+ [PREFS.USE_ACCESSIBILITY_THEME, 0],
+ [PREFS.PREFERS_CONTRAST_ENABLED, true],
+];
+
+const HIGH_CONTRAST_MODE_ON = [
+ [PREFS.USE_ACCESSIBILITY_THEME, 1],
+ [PREFS.PREFERS_CONTRAST_ENABLED, true],
+];
+
+add_task(async function testReportSentViewBGColor() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonDisabled();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ const { defaultView } = browser.ownerGlobal.document;
+
+ const menu = AppMenu();
+
+ await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_OFF });
+ const rbs = await menu.openReportBrokenSite();
+ const { mainView, sentView } = rbs;
+ mainView.style.backgroundColor = "var(--color-background-success)";
+ const expectedReportSentBGColor =
+ defaultView.getComputedStyle(mainView).backgroundColor;
+ mainView.style.backgroundColor = "";
+ const expectedPrefersReducedBGColor =
+ defaultView.getComputedStyle(mainView).backgroundColor;
+
+ await rbs.clickSend();
+ is(
+ defaultView.getComputedStyle(sentView).backgroundColor,
+ expectedReportSentBGColor,
+ "Using green bgcolor when not prefers-contrast"
+ );
+ await rbs.clickOkay();
+
+ await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_ON });
+ await menu.openReportBrokenSite();
+ await rbs.clickSend();
+ is(
+ defaultView.getComputedStyle(sentView).backgroundColor,
+ expectedPrefersReducedBGColor,
+ "Using default bgcolor when prefers-contrast"
+ );
+ await rbs.clickOkay();
+ }
+ );
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js
new file mode 100644
index 0000000000..0f5545fcc4
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the reason dropdown is shown or hidden
+ * based on its pref, and that its optional and required modes affect
+ * the Send button and report appropriately.
+ */
+
+"use strict";
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+async function clickSendAndCheckPing(rbs, expectedReason = null) {
+ const pingCheck = new Promise(resolve => {
+ GleanPings.brokenSiteReport.testBeforeNextSubmit(() => {
+ Assert.equal(
+ Glean.brokenSiteReport.breakageCategory.testGetValue(),
+ expectedReason
+ );
+ resolve();
+ });
+ });
+ await rbs.clickSend();
+ return pingCheck;
+}
+
+add_task(async function testReasonDropdown() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ ensureReasonDisabled();
+
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonHidden();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ ensureReasonOptional();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ rbs.chooseReason("slow");
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "slow");
+ await rbs.clickOkay();
+
+ ensureReasonRequired();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonRequired();
+ await rbs.isSendButtonEnabled();
+ const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window);
+ await selectPromise;
+ rbs.chooseReason("media");
+ await rbs.dismissDropdownPopup();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "media");
+ await rbs.clickOkay();
+ }
+ );
+});
+
+async function getListItems(rbs) {
+ const items = Array.from(rbs.reasonInput.querySelectorAll("option")).map(i =>
+ i.id.replace("report-broken-site-popup-reason-", "")
+ );
+ Assert.equal(items[0], "choose", "First option is always 'choose'");
+ return items.join(",");
+}
+
+add_task(async function testReasonDropdownRandomized() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const USER_ID_PREF = "app.normandy.user_id";
+ const RANDOMIZE_PREF = "ui.new-webcompat-reporter.reason-dropdown.randomized";
+
+ const origNormandyUserID = Services.prefs.getCharPref(
+ USER_ID_PREF,
+ undefined
+ );
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ // confirm that the default order is initially used
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ const rbs = await AppMenu().openReportBrokenSite();
+ const defaultOrder = [
+ "choose",
+ "slow",
+ "media",
+ "content",
+ "account",
+ "adblockers",
+ "other",
+ ];
+ Assert.deepEqual(
+ await getListItems(rbs),
+ defaultOrder,
+ "non-random order is correct"
+ );
+
+ // confirm that a random order happens per user
+ let randomOrder;
+ let isRandomized = false;
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, true);
+
+ // This becomes ClientEnvironment.randomizationId, which we can set to
+ // any value which results in a different order from the default ordering.
+ Services.prefs.setCharPref("app.normandy.user_id", "dummy");
+
+ // clicking cancel triggers a reset, which is when the randomization
+ // logic is called. so we must click cancel after pref-changes here.
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ randomOrder = await getListItems(rbs);
+ Assert.ok(
+ randomOrder != defaultOrder,
+ "options are randomized with pref on"
+ );
+
+ // confirm that the order doesn't change per user
+ isRandomized = false;
+ for (let attempt = 0; attempt < 5; ++attempt) {
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ const order = await getListItems(rbs);
+
+ if (order != randomOrder) {
+ isRandomized = true;
+ break;
+ }
+ }
+ Assert.ok(!isRandomized, "options keep the same order per user");
+
+ // confirm that the order reverts to the default if pref flipped to false
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ Assert.deepEqual(
+ defaultOrder,
+ await getListItems(rbs),
+ "reverts to non-random order correctly"
+ );
+ rbs.clickCancel();
+ }
+ );
+
+ Services.prefs.setCharPref(USER_ID_PREF, origNormandyUserID);
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_report_send.js b/browser/components/reportbrokensite/test/browser/browser_report_send.js
new file mode 100644
index 0000000000..bf849776d7
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_report_send.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that sending or canceling reports with
+ * the Send and Cancel buttons work (as well as the Okay button)
+ */
+
+/* import-globals-from send.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send.js",
+ this
+);
+
+add_common_setup();
+
+requestLongerTimeout(10);
+
+async function testCancel(menu, url, description) {
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+ await rbs.clickCancel();
+ ok(!rbs.opened, "clicking Cancel closes Report Broken Site");
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+}
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab1, AppMenu());
+
+ const tab2 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab2, ProtectionsPanel(), {
+ url: "https://test.org/test/#fake",
+ breakageCategory: "media",
+ description: "test description",
+ });
+
+ closeTab(tab1);
+ closeTab(tab2);
+});
+
+add_task(async function testCancelButton() {
+ ensureReportBrokenSitePreffedOn();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testCancel(AppMenu());
+ await testCancel(ProtectionsPanel());
+ await testCancel(HelpMenu());
+
+ const tab2 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testCancel(AppMenu());
+ await testCancel(ProtectionsPanel());
+ await testCancel(HelpMenu());
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const tab3 = await openTab(REPORTABLE_PAGE_URL2, win2);
+
+ await testCancel(AppMenu(win2));
+ await testCancel(ProtectionsPanel(win2));
+ await testCancel(HelpMenu(win2));
+
+ closeTab(tab3);
+ await BrowserTestUtils.closeWindow(win2);
+
+ closeTab(tab1);
+ closeTab(tab2);
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js
new file mode 100644
index 0000000000..26101d77b9
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that when Report Broken Site is active,
+ * Report Site Issue is hidden.
+ */
+
+"use strict";
+
+add_common_setup();
+
+async function testDisabledByReportBrokenSite(menu) {
+ ensureReportBrokenSitePreffedOn();
+ ensureReportSiteIssuePreffedOn();
+
+ await menu.open();
+ menu.isReportSiteIssueHidden();
+ await menu.close();
+}
+
+async function testDisabledByPref(menu) {
+ ensureReportBrokenSitePreffedOff();
+ ensureReportSiteIssuePreffedOff();
+
+ await menu.open();
+ menu.isReportSiteIssueHidden();
+ await menu.close();
+}
+
+async function testDisabledForInvalidURLs(menu) {
+ ensureReportBrokenSitePreffedOff();
+ ensureReportSiteIssuePreffedOn();
+
+ await menu.open();
+ menu.isReportSiteIssueDisabled();
+ await menu.close();
+}
+
+async function testEnabledForValidURLs(menu) {
+ ensureReportBrokenSitePreffedOff();
+ ensureReportSiteIssuePreffedOn();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ await menu.open();
+ menu.isReportSiteIssueEnabled();
+ await menu.close();
+ }
+ );
+}
+
+// AppMenu help sub-menu
+
+add_task(async function testDisabledByReportBrokenSiteAppMenuHelpSubmenu() {
+ await testDisabledByReportBrokenSite(AppMenuHelpSubmenu());
+});
+
+// disabled for now due to bug 1775402
+//add_task(async function testDisabledByPrefAppMenuHelpSubmenu() {
+// await testDisabledByPref(AppMenuHelpSubmenu());
+//});
+
+add_task(async function testDisabledForInvalidURLsAppMenuHelpSubmenu() {
+ await testDisabledForInvalidURLs(AppMenuHelpSubmenu());
+});
+
+add_task(async function testEnabledForValidURLsAppMenuHelpSubmenu() {
+ await testEnabledForValidURLs(AppMenuHelpSubmenu());
+});
+
+// Help menu
+
+add_task(async function testDisabledByReportBrokenSiteHelpMenu() {
+ await testDisabledByReportBrokenSite(HelpMenu());
+});
+
+// disabled for now due to bug 1775402
+//add_task(async function testDisabledByPrefHelpMenu() {
+// await testDisabledByPref(HelpMenu());
+//});
+
+add_task(async function testDisabledForInvalidURLsHelpMenu() {
+ await testDisabledForInvalidURLs(HelpMenu());
+});
+
+add_task(async function testEnabledForValidURLsHelpMenu() {
+ await testEnabledForValidURLs(HelpMenu());
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js
new file mode 100644
index 0000000000..edce03e0e0
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that the send more info link appears only when its pref
+ * is set to true, and that when clicked it will open a tab to
+ * the webcompat.com endpoint and send the right data.
+ */
+
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+const VIDEO_URL = `${BASE_URL}/videotest.mp4`;
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+add_task(async function testSendMoreInfoPref() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
+
+ ensureSendMoreInfoDisabled();
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoHidden();
+ await rbs.close();
+
+ ensureSendMoreInfoEnabled();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoShown();
+ await rbs.close();
+ }
+ );
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSendMoreInfo(tab, AppMenu());
+
+ await changeTab(tab, REPORTABLE_PAGE_URL2);
+
+ await testSendMoreInfo(tab, ProtectionsPanel(), {
+ url: "https://override.com",
+ description: "another",
+ expectNoTabDetails: true,
+ });
+
+ // also load a video to ensure system codec
+ // information is loaded and properly sent
+ const tab2 = await openTab(VIDEO_URL);
+ await testSendMoreInfo(tab2, HelpMenu());
+ closeTab(tab2);
+
+ closeTab(tab);
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_site_not_working_fallback.js b/browser/components/reportbrokensite/test/browser/browser_site_not_working_fallback.js
new file mode 100644
index 0000000000..e424a14be9
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_site_not_working_fallback.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that when Report Broken Site is active,
+ * "Site not working?" is hidden on the protections panel.
+ */
+
+"use strict";
+
+add_common_setup();
+
+const TP_PREF = "privacy.trackingprotection.enabled";
+
+const TRACKING_PAGE =
+ "https://tracking.example.org/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+
+const SITE_NOT_WORKING = "protections-popup-tp-switch-section-footer";
+
+add_task(async function testSiteNotWorking() {
+ await SpecialPowers.pushPrefEnv({ set: [[TP_PREF, true]] });
+ await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function () {
+ const menu = ProtectionsPanel();
+
+ ensureReportBrokenSitePreffedOn();
+ await menu.open();
+ const siteNotWorking = document.getElementById(SITE_NOT_WORKING);
+ isMenuItemHidden(siteNotWorking, "Site not working is hidden");
+ await menu.close();
+
+ ensureReportBrokenSitePreffedOff();
+ await menu.open();
+ isMenuItemEnabled(siteNotWorking, "Site not working is shown");
+ await menu.close();
+ });
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js
new file mode 100644
index 0000000000..d398c7c4d3
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests of the expected tab key element focus order */
+
+"use strict";
+
+add_common_setup();
+
+async function ensureTabOrder(order, win = window) {
+ const config = { window: win };
+ for (let matches of order) {
+ // We need to tab through all elements in each match array in any order
+ if (!Array.isArray(matches)) {
+ matches = [matches];
+ }
+ let matchesLeft = matches.length;
+ while (matchesLeft--) {
+ const target = await pressKeyAndGetFocus("VK_TAB", config);
+ let foundMatch = false;
+ for (const [i, selector] of matches.entries()) {
+ foundMatch = selector && target.matches(selector);
+ if (foundMatch) {
+ matches[i] = "";
+ break;
+ }
+ }
+ ok(
+ foundMatch,
+ `Expected [${matches}] next, got id=${target.id}, class=${target.className}, ${target}`
+ );
+ if (!foundMatch) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+async function ensureExpectedTabOrder(
+ expectBackButton,
+ expectReason,
+ expectSendMoreInfo
+) {
+ const { activeElement } = window.document;
+ is(
+ activeElement?.id,
+ "report-broken-site-popup-url",
+ "URL is already focused"
+ );
+ const order = [];
+ if (expectReason) {
+ order.push("#report-broken-site-popup-reason");
+ }
+ order.push("#report-broken-site-popup-description");
+ if (expectSendMoreInfo) {
+ order.push("#report-broken-site-popup-send-more-info-link");
+ }
+ // moz-button-groups swap the order of buttons to follow
+ // platform conventions, so the order of send/cancel will vary.
+ order.push([
+ "#report-broken-site-popup-cancel-button",
+ "#report-broken-site-popup-send-button",
+ ]);
+ if (expectBackButton) {
+ order.push(".subviewbutton-back");
+ }
+ order.push("#report-broken-site-popup-url"); // check that we've cycled back
+ return ensureTabOrder(order);
+}
+
+async function testTabOrder(menu) {
+ ensureReasonDisabled();
+ ensureSendMoreInfoDisabled();
+
+ const { showsBackButton } = menu;
+
+ let rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, false);
+ await rbs.close();
+
+ ensureSendMoreInfoEnabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, true);
+ await rbs.close();
+
+ ensureReasonOptional();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.close();
+
+ ensureReasonRequired();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.close();
+ rbs = await menu.openReportBrokenSite();
+ rbs.chooseReason("slow");
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.clickCancel();
+
+ ensureSendMoreInfoDisabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.close();
+ rbs = await menu.openReportBrokenSite();
+ rbs.chooseReason("slow");
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.clickCancel();
+
+ ensureReasonOptional();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.close();
+
+ ensureReasonDisabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, false);
+ await rbs.close();
+}
+
+add_task(async function testTabOrdering() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ await testTabOrder(AppMenu());
+ await testTabOrder(ProtectionsPanel());
+ await testTabOrder(HelpMenu());
+ }
+ );
+});
diff --git a/browser/components/reportbrokensite/test/browser/browser_tab_switch_handling.js b/browser/components/reportbrokensite/test/browser/browser_tab_switch_handling.js
new file mode 100644
index 0000000000..db1190b893
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/browser_tab_switch_handling.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that Report Broken Site popups will be
+ * reset to whichever tab the user is on as they change
+ * between windows and tabs. */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testResetsProperlyOnTabSwitch() {
+ ensureReportBrokenSitePreffedOn();
+
+ const badTab = await openTab("about:blank");
+ const goodTab1 = await openTab(REPORTABLE_PAGE_URL);
+ const goodTab2 = await openTab(REPORTABLE_PAGE_URL2);
+
+ const appMenu = AppMenu();
+ const protPanel = ProtectionsPanel();
+
+ let rbs = await appMenu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ gBrowser.selectedTab = goodTab1;
+
+ rbs = await protPanel.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ gBrowser.selectedTab = badTab;
+ await appMenu.open();
+ appMenu.isReportBrokenSiteDisabled();
+ await appMenu.close();
+
+ gBrowser.selectedTab = goodTab1;
+ rbs = await protPanel.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ closeTab(badTab);
+ closeTab(goodTab1);
+ closeTab(goodTab2);
+});
+
+add_task(async function testResetsProperlyOnWindowSwitch() {
+ ensureReportBrokenSitePreffedOn();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const tab2 = await openTab(REPORTABLE_PAGE_URL2, win2);
+
+ const appMenu1 = AppMenu();
+ const appMenu2 = ProtectionsPanel(win2);
+
+ let rbs2 = await appMenu2.openReportBrokenSite();
+ rbs2.isMainViewResetToCurrentTab();
+ rbs2.close();
+
+ // flip back to tab1's window and ensure its URL pops up instead of tab2's URL
+ await switchToWindow(window);
+ isSelectedTab(window, tab1); // sanity check
+
+ let rbs1 = await appMenu1.openReportBrokenSite();
+ rbs1.isMainViewResetToCurrentTab();
+ rbs1.close();
+
+ // likewise flip back to tab2's window and ensure its URL pops up instead of tab1's URL
+ await switchToWindow(win2);
+ isSelectedTab(win2, tab2); // sanity check
+
+ rbs2 = await appMenu2.openReportBrokenSite();
+ rbs2.isMainViewResetToCurrentTab();
+ rbs2.close();
+
+ closeTab(tab1);
+ closeTab(tab2);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/components/reportbrokensite/test/browser/example_report_page.html b/browser/components/reportbrokensite/test/browser/example_report_page.html
new file mode 100644
index 0000000000..07602e3fbe
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/example_report_page.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<!-- 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/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <script>
+ window.marfeel = 1;
+ window.Mobify = { Tag: 1 };
+ window.FastClick = 1;
+ </script>
+ </head>
+ <body>
+ <!-- blocked tracking content -->
+ <iframe src="https://trackertest.org/"></iframe>
+ <!-- mixed active content -->
+ <iframe src="http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html"></iframe>
+ <!-- mixed display content -->
+ <img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/browser/components/reportbrokensite/test/browser/head.js b/browser/components/reportbrokensite/test/browser/head.js
new file mode 100644
index 0000000000..e3f4451b5b
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/head.js
@@ -0,0 +1,863 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+const BASE_URL =
+ "https://example.com/browser/browser/components/reportbrokensite/test/browser/";
+
+const REPORTABLE_PAGE_URL = "https://example.com";
+
+const REPORTABLE_PAGE_URL2 = REPORTABLE_PAGE_URL.replace(".com", ".org");
+
+const REPORTABLE_PAGE_URL3 = `${BASE_URL}example_report_page.html`;
+
+const NEW_REPORT_ENDPOINT_TEST_URL = `${BASE_URL}sendMoreInfoTestEndpoint.html`;
+
+const PREFS = {
+ DATAREPORTING_ENABLED: "datareporting.healthreport.uploadEnabled",
+ REPORTER_ENABLED: "ui.new-webcompat-reporter.enabled",
+ REASON: "ui.new-webcompat-reporter.reason-dropdown",
+ SEND_MORE_INFO: "ui.new-webcompat-reporter.send-more-info-link",
+ NEW_REPORT_ENDPOINT: "ui.new-webcompat-reporter.new-report-endpoint",
+ REPORT_SITE_ISSUE_ENABLED: "extensions.webcompat-reporter.enabled",
+ PREFERS_CONTRAST_ENABLED: "layout.css.prefers-contrast.enabled",
+ USE_ACCESSIBILITY_THEME: "ui.useAccessibilityTheme",
+};
+
+function add_common_setup() {
+ add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREFS.NEW_REPORT_ENDPOINT, NEW_REPORT_ENDPOINT_TEST_URL]],
+ });
+ registerCleanupFunction(function () {
+ for (const prefName of Object.values(PREFS)) {
+ Services.prefs.clearUserPref(prefName);
+ }
+ });
+ });
+}
+
+function areObjectsEqual(actual, expected, path = "") {
+ if (typeof expected == "function") {
+ try {
+ const passes = expected(actual);
+ if (!passes) {
+ info(`${path} not pass check function: ${actual}`);
+ }
+ return passes;
+ } catch (e) {
+ info(`${path} threw exception:
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}
+ exception: ${e.message}
+ ${e.stack}`);
+ return false;
+ }
+ }
+
+ if (typeof actual != typeof expected) {
+ info(`${path} types do not match:
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}`);
+ return false;
+ }
+ if (typeof actual != "object" || actual === null || expected === null) {
+ if (actual !== expected) {
+ info(`${path} does not match
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}`);
+ return false;
+ }
+ return true;
+ }
+ const prefix = path ? `${path}.` : path;
+ for (const [key, val] of Object.entries(actual)) {
+ if (!(key in expected)) {
+ info(`Extra ${prefix}${key}: ${val}`);
+ return false;
+ }
+ }
+ let result = true;
+ for (const [key, expectedVal] of Object.entries(expected)) {
+ if (key in actual) {
+ if (!areObjectsEqual(actual[key], expectedVal, `${prefix}${key}`)) {
+ result = false;
+ }
+ } else {
+ info(`Missing ${prefix}${key} (${expectedVal})`);
+ result = false;
+ }
+ }
+ return result;
+}
+
+function clickAndAwait(toClick, evt, target) {
+ const menuPromise = BrowserTestUtils.waitForEvent(target, evt);
+ EventUtils.synthesizeMouseAtCenter(toClick, {}, window);
+ return menuPromise;
+}
+
+async function openTab(url, win) {
+ const options = {
+ gBrowser:
+ win?.gBrowser ||
+ Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
+ url,
+ };
+ return BrowserTestUtils.openNewForegroundTab(options);
+}
+
+async function changeTab(tab, url) {
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+}
+
+function closeTab(tab) {
+ BrowserTestUtils.removeTab(tab);
+}
+
+function switchToWindow(win) {
+ const promises = [
+ BrowserTestUtils.waitForEvent(win, "focus"),
+ BrowserTestUtils.waitForEvent(win, "activate"),
+ ];
+ win.focus();
+ return Promise.all(promises);
+}
+
+function isSelectedTab(win, tab) {
+ const selectedTab = win.document.querySelector(".tabbrowser-tab[selected]");
+ is(selectedTab, tab);
+}
+
+function ensureReportBrokenSitePreffedOn() {
+ Services.prefs.setBoolPref(PREFS.DATAREPORTING_ENABLED, true);
+ Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, true);
+ ensureReasonDisabled();
+}
+
+function ensureReportBrokenSitePreffedOff() {
+ Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, false);
+}
+
+function ensureReportSiteIssuePreffedOn() {
+ Services.prefs.setBoolPref(PREFS.REPORT_SITE_ISSUE_ENABLED, true);
+}
+
+function ensureReportSiteIssuePreffedOff() {
+ Services.prefs.setBoolPref(PREFS.REPORT_SITE_ISSUE_ENABLED, false);
+}
+
+function ensureSendMoreInfoEnabled() {
+ Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, true);
+}
+
+function ensureSendMoreInfoDisabled() {
+ Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, false);
+}
+
+function ensureReasonDisabled() {
+ Services.prefs.setIntPref(PREFS.REASON, 0);
+}
+
+function ensureReasonOptional() {
+ Services.prefs.setIntPref(PREFS.REASON, 1);
+}
+
+function ensureReasonRequired() {
+ Services.prefs.setIntPref(PREFS.REASON, 2);
+}
+
+function isMenuItemEnabled(menuItem, itemDesc) {
+ ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
+ ok(!menuItem.disabled, `${itemDesc} menu item is enabled`);
+}
+
+function isMenuItemHidden(menuItem, itemDesc) {
+ ok(
+ !menuItem || menuItem.hidden || !BrowserTestUtils.isVisible(menuItem),
+ `${itemDesc} menu item is hidden`
+ );
+}
+
+function isMenuItemDisabled(menuItem, itemDesc) {
+ ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
+ ok(menuItem.disabled, `${itemDesc} menu item is disabled`);
+}
+
+class ReportBrokenSiteHelper {
+ sourceMenu = undefined;
+ win = undefined;
+
+ constructor(sourceMenu) {
+ this.sourceMenu = sourceMenu;
+ this.win = sourceMenu.win;
+ }
+
+ getViewNode(id) {
+ return PanelMultiView.getViewNode(this.win.document, id);
+ }
+
+ get mainView() {
+ return this.getViewNode("report-broken-site-popup-mainView");
+ }
+
+ get sentView() {
+ return this.getViewNode("report-broken-site-popup-reportSentView");
+ }
+
+ get openPanel() {
+ return this.mainView?.closest("panel");
+ }
+
+ get opened() {
+ return this.openPanel?.hasAttribute("panelopen");
+ }
+
+ async open(triggerMenuItem) {
+ const window = triggerMenuItem.ownerGlobal;
+ const shownPromise = BrowserTestUtils.waitForEvent(
+ this.mainView,
+ "ViewShown"
+ );
+ const focusPromise = BrowserTestUtils.waitForEvent(this.URLInput, "focus");
+ await EventUtils.synthesizeMouseAtCenter(triggerMenuItem, {}, window);
+ await shownPromise;
+ await focusPromise;
+ }
+
+ async #assertClickAndViewChanges(button, view, newView, newFocus) {
+ ok(view.closest("panel").hasAttribute("panelopen"), "Panel is open");
+ ok(BrowserTestUtils.isVisible(button), "Button is visible");
+ ok(!button.disabled, "Button is enabled");
+ const promises = [];
+ if (newView) {
+ if (newView.nodeName == "panel") {
+ promises.push(BrowserTestUtils.waitForEvent(newView, "popupshown"));
+ } else {
+ promises.push(BrowserTestUtils.waitForEvent(newView, "ViewShown"));
+ }
+ } else {
+ promises.push(BrowserTestUtils.waitForEvent(view, "ViewHiding"));
+ }
+ if (newFocus) {
+ promises.push(BrowserTestUtils.waitForEvent(newFocus, "focus"));
+ }
+ EventUtils.synthesizeMouseAtCenter(button, {}, this.win);
+ await Promise.all(promises);
+ }
+
+ async awaitReportSentViewOpened() {
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"),
+ BrowserTestUtils.waitForEvent(this.okayButton, "focus"),
+ ]);
+ }
+
+ async clickSend() {
+ await this.#assertClickAndViewChanges(
+ this.sendButton,
+ this.mainView,
+ this.sentView,
+ this.okayButton
+ );
+ }
+
+ waitForSendMoreInfoTab() {
+ return BrowserTestUtils.waitForNewTab(
+ this.win.gBrowser,
+ NEW_REPORT_ENDPOINT_TEST_URL
+ );
+ }
+
+ async clickSendMoreInfo() {
+ const newTabPromise = this.waitForSendMoreInfoTab();
+ EventUtils.synthesizeMouseAtCenter(this.sendMoreInfoLink, {}, this.win);
+ const newTab = await newTabPromise;
+ const receivedData = await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ [],
+ async function () {
+ await content.wrappedJSObject.messageArrived;
+ return content.wrappedJSObject.message;
+ }
+ );
+ this.win.gBrowser.removeCurrentTab();
+ return receivedData;
+ }
+
+ async clickCancel() {
+ await this.#assertClickAndViewChanges(this.cancelButton, this.mainView);
+ }
+
+ async clickOkay() {
+ await this.#assertClickAndViewChanges(this.okayButton, this.sentView);
+ }
+
+ async clickBack() {
+ await this.#assertClickAndViewChanges(
+ this.backButton,
+ this.sourceMenu.popup
+ );
+ }
+
+ isBackButtonEnabled() {
+ ok(BrowserTestUtils.isVisible(this.backButton), "Back button is visible");
+ ok(!this.backButton.disabled, "Back button is enabled");
+ }
+
+ close() {
+ if (this.opened) {
+ this.openPanel?.hidePopup(false);
+ }
+ this.sourceMenu?.close();
+ }
+
+ // UI element getters
+ get URLInput() {
+ return this.getViewNode("report-broken-site-popup-url");
+ }
+
+ get URLInvalidMessage() {
+ return this.getViewNode("report-broken-site-popup-invalid-url-msg");
+ }
+
+ get reasonInput() {
+ return this.getViewNode("report-broken-site-popup-reason");
+ }
+
+ get reasonDropdownPopup() {
+ return this.win.document.getElementById("ContentSelectDropdown").menupopup;
+ }
+
+ get reasonRequiredMessage() {
+ return this.getViewNode("report-broken-site-popup-missing-reason-msg");
+ }
+
+ get reasonLabelRequired() {
+ return this.getViewNode("report-broken-site-popup-reason-label");
+ }
+
+ get reasonLabelOptional() {
+ return this.getViewNode("report-broken-site-popup-reason-optional-label");
+ }
+
+ get descriptionTextarea() {
+ return this.getViewNode("report-broken-site-popup-description");
+ }
+
+ get sendMoreInfoLink() {
+ return this.getViewNode("report-broken-site-popup-send-more-info-link");
+ }
+
+ get backButton() {
+ return this.mainView.querySelector(".subviewbutton-back");
+ }
+
+ get sendButton() {
+ return this.getViewNode("report-broken-site-popup-send-button");
+ }
+
+ get cancelButton() {
+ return this.getViewNode("report-broken-site-popup-cancel-button");
+ }
+
+ get okayButton() {
+ return this.getViewNode("report-broken-site-popup-okay-button");
+ }
+
+ // Test helpers
+
+ #setInput(input, value) {
+ input.value = value;
+ input.dispatchEvent(
+ new UIEvent("input", { bubbles: true, view: this.win })
+ );
+ }
+
+ setURL(value) {
+ this.#setInput(this.URLInput, value);
+ }
+
+ chooseReason(value) {
+ const item = this.getViewNode(`report-broken-site-popup-reason-${value}`);
+ this.reasonInput.selectedIndex = item.index;
+ }
+
+ dismissDropdownPopup() {
+ const popup = this.reasonDropdownPopup;
+ const menuPromise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+ popup.hidePopup();
+ return menuPromise;
+ }
+
+ setDescription(value) {
+ this.#setInput(this.descriptionTextarea, value);
+ }
+
+ isURL(expected) {
+ is(this.URLInput.value, expected);
+ }
+
+ isURLInvalidMessageShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.URLInvalidMessage),
+ "'Please enter a valid URL' message is shown"
+ );
+ }
+
+ isURLInvalidMessageHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.URLInvalidMessage),
+ "'Please enter a valid URL' message is hidden"
+ );
+ }
+
+ isReasonNeededMessageShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonRequiredMessage),
+ "'Please choose a reason' message is shown"
+ );
+ }
+
+ isReasonNeededMessageHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonRequiredMessage),
+ "'Please choose a reason' message is hidden"
+ );
+ }
+
+ isSendButtonEnabled() {
+ ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
+ ok(!this.sendButton.disabled, "Send button is enabled");
+ }
+
+ isSendButtonDisabled() {
+ ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
+ ok(this.sendButton.disabled, "Send button is disabled");
+ }
+
+ isSendMoreInfoShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.sendMoreInfoLink),
+ "send more info is shown"
+ );
+ }
+
+ isSendMoreInfoHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.sendMoreInfoLink),
+ "send more info is hidden"
+ );
+ }
+
+ isSendMoreInfoShownOrHiddenAppropriately() {
+ if (Services.prefs.getBoolPref(PREFS.SEND_MORE_INFO)) {
+ this.isSendMoreInfoShown();
+ } else {
+ this.isSendMoreInfoHidden();
+ }
+ }
+
+ isReasonHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is hidden"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is hidden"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is hidden"
+ );
+ }
+
+ isReasonRequired() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is shown"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is hidden"
+ );
+ ok(
+ BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is shown"
+ );
+ }
+
+ isReasonOptional() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is shown"
+ );
+ ok(
+ BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is shown"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is hidden"
+ );
+ }
+
+ isReasonShownOrHiddenAppropriately() {
+ const pref = Services.prefs.getIntPref(PREFS.REASON);
+ if (pref == 2) {
+ this.isReasonOptional();
+ } else if (pref == 1) {
+ this.isReasonOptional();
+ } else {
+ this.isReasonHidden();
+ }
+ }
+
+ isDescription(expected) {
+ return this.descriptionTextarea.value == expected;
+ }
+
+ isMainViewResetToCurrentTab() {
+ this.isURL(this.win.gBrowser.selectedBrowser.currentURI.spec);
+ this.isDescription("");
+ this.isReasonShownOrHiddenAppropriately();
+ this.isSendMoreInfoShownOrHiddenAppropriately();
+ }
+}
+
+class MenuHelper {
+ menuDescription = undefined;
+
+ win = undefined;
+
+ constructor(win = window) {
+ this.win = win;
+ }
+
+ getViewNode(id) {
+ return PanelMultiView.getViewNode(this.win.document, id);
+ }
+
+ get showsBackButton() {
+ return true;
+ }
+
+ get reportBrokenSite() {}
+
+ get reportSiteIssue() {}
+
+ get popup() {}
+
+ get opened() {
+ return this.popup?.hasAttribute("panelopen");
+ }
+
+ async open() {}
+
+ async close() {}
+
+ isReportBrokenSiteDisabled() {
+ return isMenuItemDisabled(this.reportBrokenSite, this.menuDescription);
+ }
+
+ isReportBrokenSiteEnabled() {
+ return isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
+ }
+
+ isReportBrokenSiteHidden() {
+ return isMenuItemHidden(this.reportBrokenSite, this.menuDescription);
+ }
+
+ isReportSiteIssueDisabled() {
+ return isMenuItemDisabled(this.reportSiteIssue, this.menuDescription);
+ }
+
+ isReportSiteIssueEnabled() {
+ return isMenuItemEnabled(this.reportSiteIssue, this.menuDescription);
+ }
+
+ isReportSiteIssueHidden() {
+ return isMenuItemHidden(this.reportSiteIssue, this.menuDescription);
+ }
+
+ async openReportBrokenSite() {
+ if (!this.opened) {
+ await this.open();
+ }
+ isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
+ const rbs = new ReportBrokenSiteHelper(this);
+ await rbs.open(this.reportBrokenSite);
+ return rbs;
+ }
+
+ async openAndPrefillReportBrokenSite(url = null, description = "") {
+ let rbs = await this.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ if (url) {
+ rbs.setURL(url);
+ }
+ if (description) {
+ rbs.setDescription(description);
+ }
+ return rbs;
+ }
+}
+
+class AppMenuHelper extends MenuHelper {
+ menuDescription = "AppMenu";
+
+ get reportBrokenSite() {
+ return this.getViewNode("appMenu-report-broken-site-button");
+ }
+
+ get reportSiteIssue() {
+ return undefined;
+ }
+
+ get popup() {
+ return this.win.document.getElementById("appMenu-popup");
+ }
+
+ async open() {
+ await new CustomizableUITestUtils(this.win).openMainMenu();
+ }
+
+ async close() {
+ if (this.opened) {
+ await new CustomizableUITestUtils(this.win).hideMainMenu();
+ }
+ }
+}
+
+class AppMenuHelpSubmenuHelper extends MenuHelper {
+ menuDescription = "AppMenu help sub-menu";
+
+ get reportBrokenSite() {
+ return this.getViewNode("appMenu_help_reportBrokenSite");
+ }
+
+ get reportSiteIssue() {
+ return this.getViewNode("appMenu_help_reportSiteIssue");
+ }
+
+ get popup() {
+ return this.win.document.getElementById("appMenu-popup");
+ }
+
+ async open() {
+ await new CustomizableUITestUtils(this.win).openMainMenu();
+
+ const anchor = this.win.document.getElementById("PanelUI-menu-button");
+ this.win.PanelUI.showHelpView(anchor);
+
+ const appMenuHelpSubview = this.getViewNode("PanelUI-helpView");
+ await BrowserTestUtils.waitForEvent(appMenuHelpSubview, "ViewShown");
+ }
+
+ async close() {
+ if (this.opened) {
+ await new CustomizableUITestUtils(this.win).hideMainMenu();
+ }
+ }
+}
+
+class HelpMenuHelper extends MenuHelper {
+ menuDescription = "Help Menu";
+
+ get showsBackButton() {
+ return false;
+ }
+
+ get reportBrokenSite() {
+ return this.win.document.getElementById("help_reportBrokenSite");
+ }
+
+ get reportSiteIssue() {
+ return this.win.document.getElementById("help_reportSiteIssue");
+ }
+
+ get popup() {
+ return this.getViewNode("PanelUI-helpView");
+ }
+
+ get helpMenu() {
+ return this.win.document.getElementById("menu_HelpPopup");
+ }
+
+ async openReportBrokenSite() {
+ // We can't actually open the Help menu properly in testing, so the best
+ // we can do to open its Report Broken Site panel is to force its DOM to be
+ // prepared, and then soft-click the Report Broken Site menuitem to open it.
+ await this.open();
+ const shownPromise = BrowserTestUtils.waitForEvent(
+ this.win,
+ "ViewShown",
+ true,
+ e => e.target.classList.contains("report-broken-site-view")
+ );
+ this.reportBrokenSite.click();
+ await shownPromise;
+ return new ReportBrokenSiteHelper(this);
+ }
+
+ async open() {
+ const { helpMenu } = this;
+ const promise = BrowserTestUtils.waitForEvent(helpMenu, "popupshown");
+
+ // This event-faking method was copied from browser_title_case_menus.js.
+ // We can't actually open the Help menu in testing, but this lets us
+ // force its DOM to be properly built.
+ helpMenu.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true }));
+ helpMenu.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
+
+ await promise;
+ }
+
+ async close() {
+ const { helpMenu } = this;
+ const promise = BrowserTestUtils.waitForPopupEvent(helpMenu, "hidden");
+
+ // (Also copied from browser_title_case_menus.js)
+ // Just for good measure, we'll fire the popuphiding/popuphidden events
+ // after we close the menupopups.
+ helpMenu.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
+ helpMenu.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
+
+ await promise;
+ }
+}
+
+class ProtectionsPanelHelper extends MenuHelper {
+ menuDescription = "Protections Panel";
+
+ get reportBrokenSite() {
+ this.win.gProtectionsHandler._initializePopup();
+ return this.getViewNode("protections-popup-report-broken-site-button");
+ }
+
+ get reportSiteIssue() {
+ return undefined;
+ }
+
+ get popup() {
+ this.win.gProtectionsHandler._initializePopup();
+ return this.win.document.getElementById("protections-popup");
+ }
+
+ async open() {
+ const promise = BrowserTestUtils.waitForEvent(
+ this.win,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+ this.win.gProtectionsHandler.showProtectionsPopup();
+ await promise;
+ }
+
+ async close() {
+ if (this.opened) {
+ const popup = this.popup;
+ const promise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+ PanelMultiView.hidePopup(popup, false);
+ await promise;
+ }
+ }
+}
+
+function AppMenu(win = window) {
+ return new AppMenuHelper(win);
+}
+
+function AppMenuHelpSubmenu(win = window) {
+ return new AppMenuHelpSubmenuHelper(win);
+}
+
+function HelpMenu(win = window) {
+ return new HelpMenuHelper(win);
+}
+
+function ProtectionsPanel(win = window) {
+ return new ProtectionsPanelHelper(win);
+}
+
+function pressKeyAndAwait(event, key, config = {}) {
+ const win = config.window || window;
+ if (!event.then) {
+ event = BrowserTestUtils.waitForEvent(win, event, config.timeout || 200);
+ }
+ EventUtils.synthesizeKey(key, config, win);
+ return event;
+}
+
+async function pressKeyAndGetFocus(key, config = {}) {
+ return (await pressKeyAndAwait("focus", key, config)).target;
+}
+
+async function tabTo(match, win = window) {
+ const config = { window: win };
+ const { activeElement } = win.document;
+ if (activeElement?.matches(match)) {
+ return activeElement;
+ }
+ let initial = await pressKeyAndGetFocus("VK_TAB", config);
+ let target = initial;
+ do {
+ if (target.matches(match)) {
+ return target;
+ }
+ target = await pressKeyAndGetFocus("VK_TAB", config);
+ } while (target && target !== initial);
+ return undefined;
+}
+
+async function setupStrictETP(fn) {
+ await UrlClassifierTestUtils.addTestTrackers();
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", true],
+ ["security.mixed_content.upgrade_display_content", false],
+ [
+ "urlclassifier.trackingTable",
+ "content-track-digest256,mochitest2-track-simple",
+ ],
+ ],
+ });
+}
+
+// copied from browser/base/content/test/protectionsUI/head.js
+function waitForContentBlockingEvent(numChanges = 1, win = null) {
+ if (!win) {
+ win = window;
+ }
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onContentBlockingEvent(webProgress, request, event) {
+ n = n + 1;
+ info(
+ `Received onContentBlockingEvent event: ${event} (${n} of ${numChanges})`
+ );
+ if (n >= numChanges) {
+ win.gBrowser.removeProgressListener(listener);
+ resolve(n);
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(listener);
+ });
+}
diff --git a/browser/components/reportbrokensite/test/browser/send.js b/browser/components/reportbrokensite/test/browser/send.js
new file mode 100644
index 0000000000..a8599741ac
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/send.js
@@ -0,0 +1,290 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Helper methods for testing sending reports with
+ * the Report Broken Site feature.
+ */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+const { Troubleshoot } = ChromeUtils.importESModule(
+ "resource://gre/modules/Troubleshoot.sys.mjs"
+);
+
+function getSysinfoProperty(propertyName, defaultValue) {
+ try {
+ return Services.sysinfo.getProperty(propertyName);
+ } catch (e) {}
+ return defaultValue;
+}
+
+function securityStringToArray(str) {
+ return str ? str.split(";") : null;
+}
+
+function getExpectedGraphicsDevices(snapshot) {
+ const { graphics } = snapshot;
+ return [
+ graphics.adapterDeviceID,
+ graphics.adapterVendorID,
+ graphics.adapterDeviceID2,
+ graphics.adapterVendorID2,
+ ]
+ .filter(i => i)
+ .sort();
+}
+
+function compareGraphicsDevices(expected, rawActual) {
+ const actual = rawActual
+ .map(({ deviceID, vendorID }) => [deviceID, vendorID])
+ .flat()
+ .filter(i => i)
+ .sort();
+ return areObjectsEqual(actual, expected);
+}
+
+function getExpectedGraphicsDrivers(snapshot) {
+ const { graphics } = snapshot;
+ const expected = [];
+ for (let i = 1; i < 3; ++i) {
+ const version = graphics[`webgl${i}Version`];
+ if (version && version != "-") {
+ expected.push(graphics[`webgl${i}Renderer`]);
+ expected.push(version);
+ }
+ }
+ return expected.filter(i => i).sort();
+}
+
+function compareGraphicsDrivers(expected, rawActual) {
+ const actual = rawActual
+ .map(({ renderer, version }) => [renderer, version])
+ .flat()
+ .filter(i => i)
+ .sort();
+ return areObjectsEqual(actual, expected);
+}
+
+function getExpectedGraphicsFeatures(snapshot) {
+ const expected = {};
+ for (let { name, log, status } of snapshot.graphics.featureLog.features) {
+ for (const item of log?.reverse() ?? []) {
+ if (item.failureId && item.status == status) {
+ status = `${status} (${item.message || item.failureId})`;
+ }
+ }
+ expected[name] = status;
+ }
+ return expected;
+}
+
+async function getExpectedWebCompatInfo(tab, snapshot, fullAppData = false) {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ const { application, graphics, intl, securitySoftware } = snapshot;
+
+ const { fissionAutoStart, memorySizeBytes, updateChannel, userAgent } =
+ application;
+
+ const app = {
+ defaultLocales: intl.localeService.available,
+ defaultUseragentString: userAgent,
+ fissionEnabled: fissionAutoStart,
+ };
+ if (fullAppData) {
+ app.applicationName = application.name;
+ app.osArchitecture = getSysinfoProperty("arch", null);
+ app.osName = getSysinfoProperty("name", null);
+ app.osVersion = getSysinfoProperty("version", null);
+ app.updateChannel = updateChannel;
+ app.version = application.version;
+ }
+
+ const hasTouchScreen = graphics.info.ApzTouchInput == 1;
+
+ const { registeredAntiVirus, registeredAntiSpyware, registeredFirewall } =
+ securitySoftware;
+
+ const browserInfo = {
+ app,
+ graphics: {
+ devicesJson(actualStr) {
+ const expected = getExpectedGraphicsDevices(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return compareGraphicsDevices(expected, JSON.parse(actualStr));
+ },
+ driversJson(actualStr) {
+ const expected = getExpectedGraphicsDrivers(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return compareGraphicsDrivers(expected, JSON.parse(actualStr));
+ },
+ featuresJson(actualStr) {
+ const expected = getExpectedGraphicsFeatures(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return areObjectsEqual(JSON.parse(actualStr), expected);
+ },
+ hasTouchScreen,
+ monitorsJson(actualStr) {
+ // We don't care about monitor data on Android right now.
+ if (AppConstants.platform == "android") {
+ return actualStr == "undefined";
+ }
+ return actualStr == JSON.stringify(gfxInfo.getMonitors());
+ },
+ },
+ prefs: {
+ cookieBehavior: Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior",
+ -1
+ ),
+ forcedAcceleratedLayers: Services.prefs.getBoolPref(
+ "layers.acceleration.force-enabled",
+ false
+ ),
+ globalPrivacyControlEnabled: Services.prefs.getBoolPref(
+ "privacy.globalprivacycontrol.enabled",
+ false
+ ),
+ installtriggerEnabled: Services.prefs.getBoolPref(
+ "extensions.InstallTrigger.enabled",
+ false
+ ),
+ opaqueResponseBlocking: Services.prefs.getBoolPref(
+ "browser.opaqueResponseBlocking",
+ false
+ ),
+ resistFingerprintingEnabled: Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting",
+ false
+ ),
+ softwareWebrender: Services.prefs.getBoolPref(
+ "gfx.webrender.software",
+ false
+ ),
+ },
+ security: {
+ antispyware: securityStringToArray(registeredAntiSpyware),
+ antivirus: securityStringToArray(registeredAntiVirus),
+ firewall: securityStringToArray(registeredFirewall),
+ },
+ system: {
+ isTablet: getSysinfoProperty("tablet", false),
+ memory: Math.round(memorySizeBytes / 1024 / 1024),
+ },
+ };
+
+ const tabInfo = await tab.linkedBrowser.ownerGlobal.SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return {
+ devicePixelRatio: `${content.devicePixelRatio}`,
+ antitracking: {
+ blockList: "basic",
+ isPrivateBrowsing: false,
+ hasTrackingContentBlocked: false,
+ hasMixedActiveContentBlocked: false,
+ hasMixedDisplayContentBlocked: false,
+ },
+ frameworks: {
+ fastclick: false,
+ marfeel: false,
+ mobify: false,
+ },
+ languages: content.navigator.languages,
+ useragentString: content.navigator.userAgent,
+ };
+ }
+ );
+
+ browserInfo.graphics.devicePixelRatio = tabInfo.devicePixelRatio;
+ delete tabInfo.devicePixelRatio;
+
+ return { browserInfo, tabInfo };
+}
+
+function extractPingData(branch) {
+ const data = {};
+ for (const [name, value] of Object.entries(branch)) {
+ data[name] = value.testGetValue();
+ }
+ return data;
+}
+
+function extractBrokenSiteReportFromGleanPing(Glean) {
+ const ping = extractPingData(Glean.brokenSiteReport);
+ ping.tabInfo = extractPingData(Glean.brokenSiteReportTabInfo);
+ ping.tabInfo.antitracking = extractPingData(
+ Glean.brokenSiteReportTabInfoAntitracking
+ );
+ ping.tabInfo.frameworks = extractPingData(
+ Glean.brokenSiteReportTabInfoFrameworks
+ );
+ ping.browserInfo = {
+ app: extractPingData(Glean.brokenSiteReportBrowserInfoApp),
+ graphics: extractPingData(Glean.brokenSiteReportBrowserInfoGraphics),
+ prefs: extractPingData(Glean.brokenSiteReportBrowserInfoPrefs),
+ security: extractPingData(Glean.brokenSiteReportBrowserInfoSecurity),
+ system: extractPingData(Glean.brokenSiteReportBrowserInfoSystem),
+ };
+ return ping;
+}
+
+async function testSend(tab, menu, expectedOverrides = {}) {
+ const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
+ const description = expectedOverrides.description ?? "";
+ const breakageCategory = expectedOverrides.breakageCategory ?? null;
+
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+
+ const snapshot = await Troubleshoot.snapshot();
+ const expected = await getExpectedWebCompatInfo(tab, snapshot);
+
+ expected.url = url;
+ expected.description = description;
+ expected.breakageCategory = breakageCategory;
+
+ if (expectedOverrides.antitracking) {
+ expected.tabInfo.antitracking = expectedOverrides.antitracking;
+ }
+
+ if (expectedOverrides.frameworks) {
+ expected.tabInfo.frameworks = expectedOverrides.frameworks;
+ }
+
+ if (breakageCategory) {
+ rbs.chooseReason(breakageCategory);
+ }
+
+ const pingCheck = new Promise(resolve => {
+ Services.fog.testResetFOG();
+ GleanPings.brokenSiteReport.testBeforeNextSubmit(() => {
+ const ping = extractBrokenSiteReportFromGleanPing(Glean);
+ ok(areObjectsEqual(ping, expected), "ping matches expectations");
+ resolve();
+ });
+ });
+
+ await rbs.clickSend();
+ await pingCheck;
+ await rbs.clickOkay();
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+}
diff --git a/browser/components/reportbrokensite/test/browser/sendMoreInfoTestEndpoint.html b/browser/components/reportbrokensite/test/browser/sendMoreInfoTestEndpoint.html
new file mode 100644
index 0000000000..39b1b3d25a
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/sendMoreInfoTestEndpoint.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<!-- 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/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <script>
+ let ready;
+ window.wrtReady = new Promise(r => ready = r);
+
+ let arrived;
+ window.messageArrived = new Promise(r => arrived = r);
+
+ window.addEventListener("message", e => {
+ window.message = e.data;
+ arrived();
+ });
+
+ window.addEventListener("load", () => {
+ setTimeout(ready, 100);
+ });
+ </script>
+ </body>
+</html>
diff --git a/browser/components/reportbrokensite/test/browser/send_more_info.js b/browser/components/reportbrokensite/test/browser/send_more_info.js
new file mode 100644
index 0000000000..6803403f63
--- /dev/null
+++ b/browser/components/reportbrokensite/test/browser/send_more_info.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Helper methods for testing the "send more info" link
+ * of the Report Broken Site feature.
+ */
+
+/* import-globals-from head.js */
+/* import-globals-from send.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send.js",
+ this
+);
+
+async function reformatExpectedWebCompatInfo(tab, overrides) {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ const snapshot = await Troubleshoot.snapshot();
+ const expected = await getExpectedWebCompatInfo(tab, snapshot, true);
+ const { browserInfo, tabInfo } = expected;
+ const { app, graphics, prefs, security } = browserInfo;
+ const {
+ applicationName,
+ defaultUseragentString,
+ fissionEnabled,
+ osArchitecture,
+ osName,
+ osVersion,
+ updateChannel,
+ version,
+ } = app;
+ const { devicePixelRatio, hasTouchScreen } = graphics;
+ const { antitracking, languages, useragentString } = tabInfo;
+
+ const atOverrides = overrides.antitracking;
+ const blockList = atOverrides?.blockList ?? antitracking.blockList;
+ const hasMixedActiveContentBlocked =
+ atOverrides?.hasMixedActiveContentBlocked ??
+ antitracking.hasMixedActiveContentBlocked;
+ const hasMixedDisplayContentBlocked =
+ atOverrides?.hasMixedDisplayContentBlocked ??
+ antitracking.hasMixedDisplayContentBlocked;
+ const hasTrackingContentBlocked =
+ atOverrides?.hasTrackingContentBlocked ??
+ antitracking.hasTrackingContentBlocked;
+ const isPrivateBrowsing =
+ atOverrides?.isPrivateBrowsing ?? antitracking.isPrivateBrowsing;
+
+ const extra_labels = [];
+ const frameworks = overrides.frameworks ?? {
+ fastclick: false,
+ mobify: false,
+ marfeel: false,
+ };
+
+ // ignore the console log unless explicily testing for it.
+ const consoleLog = overrides.consoleLog ?? (() => true);
+
+ const finalPrefs = {};
+ for (const [key, pref] of Object.entries({
+ cookieBehavior: "network.cookie.cookieBehavior",
+ forcedAcceleratedLayers: "layers.acceleration.force-enabled",
+ globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled",
+ installtriggerEnabled: "extensions.InstallTrigger.enabled",
+ opaqueResponseBlocking: "browser.opaqueResponseBlocking",
+ resistFingerprintingEnabled: "privacy.resistFingerprinting",
+ softwareWebrender: "gfx.webrender.software",
+ })) {
+ if (key in prefs) {
+ finalPrefs[pref] = prefs[key];
+ }
+ }
+
+ const reformatted = {
+ blockList,
+ details: {
+ additionalData: {
+ applicationName,
+ blockList,
+ devicePixelRatio: parseInt(devicePixelRatio),
+ finalUserAgent: useragentString,
+ fissionEnabled,
+ gfxData: {
+ devices(actual) {
+ const devices = getExpectedGraphicsDevices(snapshot);
+ return compareGraphicsDevices(devices, actual);
+ },
+ drivers(actual) {
+ const drvs = getExpectedGraphicsDrivers(snapshot);
+ return compareGraphicsDrivers(drvs, actual);
+ },
+ features(actual) {
+ const features = getExpectedGraphicsFeatures(snapshot);
+ return areObjectsEqual(actual, features);
+ },
+ hasTouchScreen,
+ monitors(actual) {
+ // We don't care about monitor data on Android right now.
+ if (AppConstants.platform === "android") {
+ return actual == undefined;
+ }
+ return areObjectsEqual(actual, gfxInfo.getMonitors());
+ },
+ },
+ hasMixedActiveContentBlocked,
+ hasMixedDisplayContentBlocked,
+ hasTrackingContentBlocked,
+ isPB: isPrivateBrowsing,
+ languages,
+ osArchitecture,
+ osName,
+ osVersion,
+ prefs: finalPrefs,
+ updateChannel,
+ userAgent: defaultUseragentString,
+ version,
+ },
+ blockList,
+ consoleLog,
+ frameworks,
+ hasTouchScreen,
+ "gfx.webrender.software": prefs.softwareWebrender,
+ "mixed active content blocked": hasMixedActiveContentBlocked,
+ "mixed passive content blocked": hasMixedDisplayContentBlocked,
+ "tracking content blocked": hasTrackingContentBlocked
+ ? `true (${blockList})`
+ : "false",
+ },
+ extra_labels,
+ src: "desktop-reporter",
+ utm_campaign: "report-broken-site",
+ utm_source: "desktop-reporter",
+ };
+
+ // We only care about this pref on Linux right now on webcompat.com.
+ if (AppConstants.platform != "linux") {
+ delete finalPrefs["layers.acceleration.force-enabled"];
+ } else {
+ reformatted.details["layers.acceleration.force-enabled"] =
+ finalPrefs["layers.acceleration.force-enabled"];
+ }
+
+ // Only bother adding the security key if it has any data
+ if (Object.values(security).filter(e => e).length) {
+ reformatted.details.additionalData.sec = security;
+ }
+
+ const expectedCodecs = snapshot.media.codecSupportInfo
+ .replaceAll(" NONE", "")
+ .split("\n")
+ .sort()
+ .join("\n");
+ if (expectedCodecs) {
+ reformatted.details.additionalData.gfxData.codecSupport = rawActual => {
+ const actual = Object.entries(rawActual)
+ .map(([name, { hardware, software }]) =>
+ `${name} ${software ? "SW" : ""} ${hardware ? "HW" : ""}`.trim()
+ )
+ .sort()
+ .join("\n");
+ return areObjectsEqual(actual, expectedCodecs);
+ };
+ }
+
+ if (blockList != "basic") {
+ extra_labels.push(`type-tracking-protection-${blockList}`);
+ }
+
+ if (overrides.expectNoTabDetails) {
+ delete reformatted.details.frameworks;
+ delete reformatted.details.consoleLog;
+ delete reformatted.details["mixed active content blocked"];
+ delete reformatted.details["mixed passive content blocked"];
+ delete reformatted.details["tracking content blocked"];
+ } else {
+ const { fastclick, mobify, marfeel } = frameworks;
+ if (fastclick) {
+ extra_labels.push("type-fastclick");
+ reformatted.details.fastclick = true;
+ }
+ if (mobify) {
+ extra_labels.push("type-mobify");
+ reformatted.details.mobify = true;
+ }
+ if (marfeel) {
+ extra_labels.push("type-marfeel");
+ reformatted.details.marfeel = true;
+ }
+ }
+
+ return reformatted;
+}
+
+async function testSendMoreInfo(tab, menu, expectedOverrides = {}) {
+ const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
+ const description = expectedOverrides.description ?? "";
+
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+
+ const receivedData = await rbs.clickSendMoreInfo();
+ const { message } = receivedData;
+
+ const expected = await reformatExpectedWebCompatInfo(tab, expectedOverrides);
+ expected.url = url;
+ expected.description = description;
+
+ ok(areObjectsEqual(message, expected), "ping matches expectations");
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+}