diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/reportbrokensite | |
parent | Initial commit. (diff) | |
download | firefox-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')
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(); +} |