summaryrefslogtreecommitdiffstats
path: root/browser/extensions/report-site-issue
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/extensions/report-site-issue/.eslintrc.js58
-rw-r--r--browser/extensions/report-site-issue/background.js217
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js39
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json35
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm163
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js197
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json64
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js38
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/helpMenu.json28
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/l10n.js55
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/l10n.json21
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js99
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json21
-rw-r--r--browser/extensions/report-site-issue/locales/en-US/webcompat.properties10
-rw-r--r--browser/extensions/report-site-issue/locales/jar.mn8
-rw-r--r--browser/extensions/report-site-issue/locales/moz.build7
-rw-r--r--browser/extensions/report-site-issue/manifest.json66
-rw-r--r--browser/extensions/report-site-issue/moz.build41
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser.ini14
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_button_state.js52
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js41
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js300
-rw-r--r--browser/extensions/report-site-issue/test/browser/fastclick.html11
-rw-r--r--browser/extensions/report-site-issue/test/browser/frameworks.html8
-rw-r--r--browser/extensions/report-site-issue/test/browser/head.js119
-rw-r--r--browser/extensions/report-site-issue/test/browser/test.html40
-rw-r--r--browser/extensions/report-site-issue/test/browser/webcompat.html85
27 files changed, 1837 insertions, 0 deletions
diff --git a/browser/extensions/report-site-issue/.eslintrc.js b/browser/extensions/report-site-issue/.eslintrc.js
new file mode 100644
index 0000000000..13b953ef04
--- /dev/null
+++ b/browser/extensions/report-site-issue/.eslintrc.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "error",
+ "mozilla/var-only-at-top-level": "error",
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Warn about cyclomatic complexity in functions.
+ complexity: ["error", { max: 26 }],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Allow the console API aside from console.log.
+ "no-console": ["error", { allow: ["error", "info", "trace", "warn"] }],
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "error",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Require use of the second argument for parseInt().
+ radix: "error",
+
+ // Require "use strict" to be defined globally in the script.
+ strict: ["error", "global"],
+
+ // Disallow Yoda conditions (where literal value comes first).
+ yoda: "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+ },
+};
diff --git a/browser/extensions/report-site-issue/background.js b/browser/extensions/report-site-issue/background.js
new file mode 100644
index 0000000000..9662f37846
--- /dev/null
+++ b/browser/extensions/report-site-issue/background.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals browser */
+
+const Config = {
+ newIssueEndpoint: "https://webcompat.com/issues/new",
+ newIssueEndpointPref: "newIssueEndpoint",
+ screenshotFormat: {
+ format: "jpeg",
+ quality: 75,
+ },
+};
+
+const FRAMEWORK_KEYS = ["hasFastClick", "hasMobify", "hasMarfeel"];
+
+browser.helpMenu.onHelpMenuCommand.addListener(tab => {
+ return getWebCompatInfoForTab(tab).then(
+ info => {
+ return openWebCompatTab(info);
+ },
+ err => {
+ console.error("WebCompat Reporter: unexpected error", err);
+ }
+ );
+});
+
+browser.aboutConfigPrefs.onEndpointPrefChange.addListener(checkEndpointPref);
+
+checkEndpointPref();
+
+async function checkEndpointPref() {
+ const value = await browser.aboutConfigPrefs.getEndpointPref();
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setEndpointPref(Config.newIssueEndpoint);
+ } else {
+ Config.newIssueEndpoint = value;
+ }
+}
+
+function hasFastClickPageScript() {
+ const win = window.wrappedJSObject;
+
+ if (win.FastClick) {
+ return true;
+ }
+
+ for (const property in win) {
+ try {
+ const proto = win[property].prototype;
+ if (proto && proto.needsClick) {
+ return true;
+ }
+ } catch (_) {}
+ }
+
+ return false;
+}
+
+function hasMobifyPageScript() {
+ const win = window.wrappedJSObject;
+ return !!(win.Mobify && win.Mobify.Tag);
+}
+
+function hasMarfeelPageScript() {
+ const win = window.wrappedJSObject;
+ return !!win.marfeel;
+}
+
+function checkForFrameworks(tabId) {
+ return browser.tabs
+ .executeScript(tabId, {
+ code: `
+ (function() {
+ ${hasFastClickPageScript};
+ ${hasMobifyPageScript};
+ ${hasMarfeelPageScript};
+
+ const result = {
+ hasFastClick: hasFastClickPageScript(),
+ hasMobify: hasMobifyPageScript(),
+ hasMarfeel: hasMarfeelPageScript(),
+ }
+
+ return result;
+ })();
+ `,
+ })
+ .then(([results]) => results)
+ .catch(() => false);
+}
+
+function getWebCompatInfoForTab(tab) {
+ const { id, url } = tab;
+ return Promise.all([
+ browser.browserInfo.getBlockList(),
+ browser.browserInfo.getBuildID(),
+ browser.browserInfo.getGraphicsPrefs(),
+ browser.browserInfo.getUpdateChannel(),
+ browser.browserInfo.hasTouchScreen(),
+ browser.tabExtras.getWebcompatInfo(id),
+ browser.browserInfo.getAdditionalData(),
+ checkForFrameworks(id),
+ browser.tabs.captureTab(id, Config.screenshotFormat).catch(e => {
+ console.error("WebCompat Reporter: getting a screenshot failed", e);
+ return Promise.resolve(undefined);
+ }),
+ ]).then(
+ ([
+ blockList,
+ buildID,
+ graphicsPrefs,
+ channel,
+ hasTouchScreen,
+ frameInfo,
+ additionalData,
+ frameworks,
+ screenshot,
+ ]) => {
+ if (channel !== "linux") {
+ delete graphicsPrefs["layers.acceleration.force-enabled"];
+ }
+
+ const consoleLog = frameInfo.log;
+ delete frameInfo.log;
+
+ additionalData.isPB = frameInfo.isPB;
+ additionalData.prefs = { ...additionalData.prefs, ...graphicsPrefs };
+ additionalData.hasMixedActiveContentBlocked =
+ frameInfo.hasMixedActiveContentBlocked;
+ additionalData.hasMixedDisplayContentBlocked =
+ frameInfo.hasMixedDisplayContentBlocked;
+ additionalData.hasTrackingContentBlocked =
+ !!frameInfo.hasTrackingContentBlocked;
+
+ return Object.assign(frameInfo, {
+ tabId: id,
+ blockList,
+ details: Object.assign(graphicsPrefs, {
+ buildID,
+ channel,
+ consoleLog,
+ frameworks,
+ additionalData,
+ hasTouchScreen,
+ "mixed active content blocked":
+ frameInfo.hasMixedActiveContentBlocked,
+ "mixed passive content blocked":
+ frameInfo.hasMixedDisplayContentBlocked,
+ "tracking content blocked": frameInfo.hasTrackingContentBlocked
+ ? `true (${blockList})`
+ : "false",
+ }),
+ screenshot,
+ url,
+ });
+ }
+ );
+}
+
+function stripNonASCIIChars(str) {
+ // eslint-disable-next-line no-control-regex
+ return str.replace(/[^\x00-\x7F]/g, "");
+}
+
+async function openWebCompatTab(compatInfo) {
+ const url = new URL(Config.newIssueEndpoint);
+ const { details } = compatInfo;
+ const params = {
+ url: `${compatInfo.url}`,
+ utm_source: "desktop-reporter",
+ utm_campaign: "report-site-issue-button",
+ src: "desktop-reporter",
+ details,
+ extra_labels: [],
+ };
+
+ for (let framework of FRAMEWORK_KEYS) {
+ if (details.frameworks[framework]) {
+ params.details[framework] = true;
+ params.extra_labels.push(
+ framework.replace(/^has/, "type-").toLowerCase()
+ );
+ }
+ }
+ delete details.frameworks;
+
+ if (details["gfx.webrender.all"] || details["gfx.webrender.enabled"]) {
+ params.extra_labels.push("type-webrender-enabled");
+ }
+ if (compatInfo.hasTrackingContentBlocked) {
+ params.extra_labels.push(
+ `type-tracking-protection-${compatInfo.blockList}`
+ );
+ }
+
+ const json = stripNonASCIIChars(JSON.stringify(params));
+ const tab = await browser.tabs.create({ url: url.href });
+ await browser.tabs.executeScript(tab.id, {
+ runAt: "document_end",
+ code: `(function() {
+ async function postMessageData(dataURI, metadata) {
+ const res = await fetch(dataURI);
+ const blob = await res.blob();
+ const data = {
+ screenshot: blob,
+ message: metadata
+ };
+ postMessage(data, "${url.origin}");
+ }
+ postMessageData("${compatInfo.screenshot}", ${json});
+ })()`,
+ });
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
new file mode 100644
index 0000000000..0e8b6346c4
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, ExtensionCommon, Services */
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+ getAPI(context) {
+ const EventManager = ExtensionCommon.EventManager;
+ const extensionIDBase = context.extension.id.split("@")[0];
+ const endpointPrefName = `extensions.${extensionIDBase}.newIssueEndpoint`;
+
+ return {
+ aboutConfigPrefs: {
+ onEndpointPrefChange: new EventManager({
+ context,
+ name: "aboutConfigPrefs.onEndpointPrefChange",
+ register: fire => {
+ const callback = () => {
+ fire.async().catch(() => {}); // ignore Message Manager disconnects
+ };
+ Services.prefs.addObserver(endpointPrefName, callback);
+ return () => {
+ Services.prefs.removeObserver(endpointPrefName, callback);
+ };
+ },
+ }).api(),
+ async getEndpointPref() {
+ return Services.prefs.getStringPref(endpointPrefName, undefined);
+ },
+ async setEndpointPref(value) {
+ Services.prefs.setStringPref(endpointPrefName, value);
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
new file mode 100644
index 0000000000..1fd313e392
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
@@ -0,0 +1,35 @@
+[
+ {
+ "namespace": "aboutConfigPrefs",
+ "description": "experimental API extension to allow access to about:config preferences",
+ "events": [
+ {
+ "name": "onEndpointPrefChange",
+ "type": "function",
+ "parameters": []
+ }
+ ],
+ "functions": [
+ {
+ "name": "getEndpointPref",
+ "type": "function",
+ "description": "Get the endpoint preference's value",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "setEndpointPref",
+ "type": "function",
+ "description": "Set the endpoint preference's value",
+ "parameters": [
+ {
+ "name": "value",
+ "type": "string",
+ "description": "The new value"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm b/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm
new file mode 100644
index 0000000000..52bc6c56c6
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm
@@ -0,0 +1,163 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ReportSiteIssueHelperChild"];
+
+const PREVIEW_MAX_ITEMS = 10;
+const LOG_LEVELS = ["debug", "info", "warn", "error"];
+
+function getPreview(value) {
+ switch (typeof value) {
+ case "symbol":
+ return value.toString();
+
+ case "function":
+ return "function ()";
+
+ case "object":
+ if (value === null) {
+ return null;
+ }
+
+ if (Array.isArray(value)) {
+ return `(${value.length})[...]`;
+ }
+
+ return "{...}";
+
+ case "undefined":
+ return "undefined";
+
+ default:
+ try {
+ structuredClone(value);
+ } catch (_) {
+ return `${value}` || "?";
+ }
+
+ return value;
+ }
+}
+
+function getArrayPreview(arr) {
+ const preview = [];
+ let count = 0;
+ for (const value of arr) {
+ if (++count > PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ preview.push(getPreview(value));
+ }
+
+ return preview;
+}
+
+function getObjectPreview(obj) {
+ const preview = {};
+ let count = 0;
+ for (const key of Object.keys(obj)) {
+ if (++count > PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ preview[key] = getPreview(obj[key]);
+ }
+
+ return preview;
+}
+
+function getArgs(value) {
+ if (typeof value === "object" && value !== null) {
+ if (Array.isArray(value)) {
+ return getArrayPreview(value);
+ }
+
+ return getObjectPreview(value);
+ }
+
+ return getPreview(value);
+}
+
+class ReportSiteIssueHelperChild extends JSWindowActorChild {
+ _getConsoleMessages(windowId) {
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ let messages = ConsoleAPIStorage.getEvents(windowId);
+ return messages.map(evt => {
+ const { columnNumber, filename, level, lineNumber, timeStamp } = evt;
+ const args = evt.arguments.map(getArgs);
+
+ const message = {
+ level,
+ log: args,
+ uri: filename,
+ pos: `${lineNumber}:${columnNumber}`,
+ };
+
+ return { timeStamp, message };
+ });
+ }
+
+ _getScriptErrors(windowId, includePrivate) {
+ const messages = Services.console.getMessageArray();
+ return messages
+ .filter(message => {
+ if (message instanceof Ci.nsIScriptError) {
+ if (!includePrivate && message.isFromPrivateWindow) {
+ return false;
+ }
+
+ if (windowId && windowId !== message.innerWindowID) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // If this is not an nsIScriptError and we need to do window-based
+ // filtering we skip this message.
+ return false;
+ })
+ .map(error => {
+ const {
+ timeStamp,
+ errorMessage,
+ sourceName,
+ lineNumber,
+ columnNumber,
+ logLevel,
+ } = error;
+ const message = {
+ level: LOG_LEVELS[logLevel],
+ log: [errorMessage],
+ uri: sourceName,
+ pos: `${lineNumber}:${columnNumber}`,
+ };
+ return { timeStamp, message };
+ });
+ }
+
+ _getLoggedMessages(includePrivate = false) {
+ const windowId = this.contentWindow.windowGlobalChild.innerWindowId;
+ return this._getConsoleMessages(windowId).concat(
+ this._getScriptErrors(windowId, includePrivate)
+ );
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "GetLog":
+ return this._getLoggedMessages();
+ case "GetBlockingStatus":
+ const { docShell } = this;
+ return {
+ hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
+ };
+ }
+ return null;
+ }
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js
new file mode 100644
index 0000000000..ce4466f1cf
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global AppConstants, ExtensionAPI, Services */
+
+function isTelemetryEnabled() {
+ return Services.prefs.getBoolPref(
+ "datareporting.healthreport.uploadEnabled",
+ false
+ );
+}
+
+function getSysinfoProperty(propertyName, defaultValue) {
+ try {
+ return Services.sysinfo.getProperty(propertyName);
+ } catch (e) {}
+
+ return defaultValue;
+}
+
+function getUserAgent() {
+ const { userAgent } = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler);
+ return userAgent;
+}
+
+function getGfxData() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ const data = {};
+
+ try {
+ const {
+ compositor,
+ hwCompositing,
+ openglCompositing,
+ wrCompositor,
+ wrSoftware,
+ } = gfxInfo.getFeatures();
+
+ data.features = {
+ compositor,
+ hwCompositing,
+ openglCompositing,
+ wrCompositor,
+ wrSoftware,
+ };
+ } catch (e) {}
+
+ try {
+ if (AppConstants.platform !== "android") {
+ data.monitors = gfxInfo.getMonitors();
+ }
+ } catch (e) {}
+
+ return data;
+}
+
+function limitStringToLength(str, maxLength) {
+ if (typeof str !== "string") {
+ return null;
+ }
+ return str.substring(0, maxLength);
+}
+
+function getSecurityAppData() {
+ const maxStringLength = 256;
+
+ const keys = [
+ ["registeredAntiVirus", "antivirus"],
+ ["registeredAntiSpyware", "antispyware"],
+ ["registeredFirewall", "firewall"],
+ ];
+
+ let result = {};
+
+ for (let [inKey, outKey] of keys) {
+ let prop = getSysinfoProperty(inKey, null);
+ if (prop) {
+ prop = limitStringToLength(prop, maxStringLength).split(";");
+ }
+
+ result[outKey] = prop;
+ }
+
+ return result;
+}
+
+function getAdditionalPrefs() {
+ const prefs = {};
+ for (const [name, dflt] of Object.entries({
+ "browser.opaqueResponseBlocking": false,
+ "extensions.InstallTrigger.enabled": false,
+ "gfx.canvas.accelerated.force-enabled": false,
+ "gfx.webrender.compositor.force-enabled": false,
+ "privacy.resistFingerprinting": false,
+ })) {
+ prefs[name] = Services.prefs.getBoolPref(name, dflt);
+ }
+ const cookieBehavior = "network.cookie.cookieBehavior";
+ prefs[cookieBehavior] = Services.prefs.getIntPref(cookieBehavior);
+
+ return prefs;
+}
+
+function getMemoryMB() {
+ let memoryMB = getSysinfoProperty("memsize", null);
+ if (memoryMB) {
+ memoryMB = Math.round(memoryMB / 1024 / 1024);
+ }
+
+ return memoryMB;
+}
+
+this.browserInfo = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ browserInfo: {
+ async getGraphicsPrefs() {
+ const prefs = {};
+ for (const [name, dflt] of Object.entries({
+ "layers.acceleration.force-enabled": false,
+ "gfx.webrender.all": false,
+ "gfx.webrender.blob-images": true,
+ "gfx.webrender.enabled": false,
+ "image.mem.shared": true,
+ })) {
+ prefs[name] = Services.prefs.getBoolPref(name, dflt);
+ }
+ return prefs;
+ },
+ async getAppVersion() {
+ return AppConstants.MOZ_APP_VERSION;
+ },
+ async getBlockList() {
+ const trackingTable = Services.prefs.getCharPref(
+ "urlclassifier.trackingTable"
+ );
+ // If content-track-digest256 is in the tracking table,
+ // the user has enabled the strict list.
+ return trackingTable.includes("content") ? "strict" : "basic";
+ },
+ async getBuildID() {
+ return Services.appinfo.appBuildID;
+ },
+ async getUpdateChannel() {
+ return AppConstants.MOZ_UPDATE_CHANNEL;
+ },
+ async getPlatform() {
+ return AppConstants.platform;
+ },
+ async hasTouchScreen() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ return gfxInfo.getInfo().ApzTouchInput == 1;
+ },
+ async getAdditionalData() {
+ const blockList = await this.getBlockList();
+ const userAgent = getUserAgent();
+ const gfxData = getGfxData();
+ const prefs = getAdditionalPrefs();
+ const memoryMb = getMemoryMB();
+
+ const data = {
+ applicationName: Services.appinfo.name,
+ version: Services.appinfo.version,
+ updateChannel: AppConstants.MOZ_UPDATE_CHANNEL,
+ osArchitecture: getSysinfoProperty("arch", null),
+ osName: getSysinfoProperty("name", null),
+ osVersion: getSysinfoProperty("version", null),
+ fissionEnabled: Services.appinfo.fissionAutostart,
+ userAgent,
+ gfxData,
+ blockList,
+ prefs,
+ memoryMb,
+ };
+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ data.sec = getSecurityAppData();
+ }
+
+ if (AppConstants.platform === "android") {
+ data.device = getSysinfoProperty("device", null);
+ data.isTablet = getSysinfoProperty("tablet", false);
+ }
+
+ return data;
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json
new file mode 100644
index 0000000000..c12f2ceb2e
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json
@@ -0,0 +1,64 @@
+[
+ {
+ "namespace": "browserInfo",
+ "description": "experimental API extensions to get browser info not exposed via web APIs",
+ "functions": [
+ {
+ "name": "getAppVersion",
+ "type": "function",
+ "description": "Gets the app version",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getBlockList",
+ "type": "function",
+ "description": "Gets the current blocklist",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getBuildID",
+ "type": "function",
+ "description": "Gets the build ID",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getGraphicsPrefs",
+ "type": "function",
+ "description": "Gets interesting about:config prefs for graphics",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getPlatform",
+ "type": "function",
+ "description": "Gets the platform",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getUpdateChannel",
+ "type": "function",
+ "description": "Gets the update channel",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "hasTouchScreen",
+ "type": "function",
+ "description": "Gets whether a touchscreen is present",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getAdditionalData",
+ "type": "function",
+ "description": "Gets additional info for the new reporter experiment",
+ "parameters": [],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js
new file mode 100644
index 0000000000..804f4b08d5
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, ExtensionCommon, Services */
+
+const TOPIC = "report-site-issue";
+
+this.helpMenu = class extends ExtensionAPI {
+ getAPI(context) {
+ const { tabManager } = context.extension;
+ let EventManager = ExtensionCommon.EventManager;
+
+ return {
+ helpMenu: {
+ onHelpMenuCommand: new EventManager({
+ context,
+ name: "helpMenu",
+ register: fire => {
+ let observer = (subject, topic, data) => {
+ let nativeTab = subject.wrappedJSObject;
+ let tab = tabManager.convert(nativeTab);
+ fire.async(tab);
+ };
+
+ Services.obs.addObserver(observer, TOPIC);
+
+ return () => {
+ Services.obs.removeObserver(observer, TOPIC);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.json b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.json
new file mode 100644
index 0000000000..e7c3a8c405
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.json
@@ -0,0 +1,28 @@
+[
+ {
+ "namespace": "helpMenu",
+ "events": [
+ {
+ "name": "onHelpMenuCommand",
+ "type": "function",
+ "async": "callback",
+ "description": "Fired when the command event for the Report Site Issue menuitem in Help is fired.",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "tab",
+ "$ref": "tabs.Tab",
+ "optional": true,
+ "description": "Details about the selected tab in the window where the menuitem command fired."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/l10n.js b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
new file mode 100644
index 0000000000..1c91e7a04d
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyGetter(this, "l10nStrings", function () {
+ return Services.strings.createBundle(
+ "chrome://report-site-issue/locale/webcompat.properties"
+ );
+});
+
+let l10nManifest;
+
+this.l10n = class extends ExtensionAPI {
+ onShutdown(isAppShutdown) {
+ if (!isAppShutdown && l10nManifest) {
+ Components.manager.removeBootstrappedManifestLocation(l10nManifest);
+ }
+ }
+ getAPI(context) {
+ // Until we move to Fluent (bug 1446164), we're stuck with
+ // chrome.manifest for handling localization since its what the
+ // build system can handle for localized repacks.
+ if (context.extension.rootURI instanceof Ci.nsIJARURI) {
+ l10nManifest = context.extension.rootURI.JARFile.QueryInterface(
+ Ci.nsIFileURL
+ ).file;
+ } else if (context.extension.rootURI instanceof Ci.nsIFileURL) {
+ l10nManifest = context.extension.rootURI.file;
+ }
+
+ if (l10nManifest) {
+ Components.manager.addBootstrappedManifestLocation(l10nManifest);
+ } else {
+ console.error(
+ "Cannot find webcompat reporter chrome.manifest for registering translated strings"
+ );
+ }
+
+ return {
+ l10n: {
+ getMessage(name) {
+ try {
+ return Promise.resolve(l10nStrings.GetStringFromName(name));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/l10n.json b/browser/extensions/report-site-issue/experimentalAPIs/l10n.json
new file mode 100644
index 0000000000..60942e726c
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.json
@@ -0,0 +1,21 @@
+[
+ {
+ "namespace": "l10n",
+ "description": "A stop-gap L10N API only meant to be used until a Fluent-based API is added in bug 1425104",
+ "functions": [
+ {
+ "name": "getMessage",
+ "type": "function",
+ "description": "Gets the message with the given name",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The name of the message"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js
new file mode 100644
index 0000000000..12ccd91e05
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, XPCOMUtils, Services */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+this.tabExtras = class extends ExtensionAPI {
+ constructor(extension) {
+ super(extension);
+ this._registerActorModule();
+ }
+
+ getAPI(context) {
+ const { tabManager } = context.extension;
+ return {
+ tabExtras: {
+ async getWebcompatInfo(tabId) {
+ const {
+ browser: { browsingContext },
+ incognito,
+ } = tabManager.get(tabId);
+ const actors = gatherActors("ReportSiteIssueHelper", browsingContext);
+ const promises = actors.map(actor => actor.sendQuery("GetLog"));
+ const logs = await Promise.all(promises);
+ const info = await actors[0].sendQuery("GetBlockingStatus");
+ info.hasMixedActiveContentBlocked = !!(
+ browsingContext.secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
+ );
+ info.hasMixedDisplayContentBlocked = !!(
+ browsingContext.secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT
+ );
+ info.isPB = incognito;
+ info.log = logs
+ .flat()
+ .sort((a, b) => a.timeStamp - b.timeStamp)
+ .map(m => m.message);
+ return info;
+ },
+ },
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ this._unregisterActorModule();
+ }
+
+ _registerActorModule() {
+ resProto.setSubstitution(
+ "report-site-issue",
+ Services.io.newURI(
+ "experimentalAPIs/actors/",
+ null,
+ this.extension.rootURI
+ )
+ );
+ ChromeUtils.registerWindowActor("ReportSiteIssueHelper", {
+ child: {
+ moduleURI: "resource://report-site-issue/tabExtrasActor.jsm",
+ },
+ allFrames: true,
+ });
+ }
+
+ _unregisterActorModule() {
+ ChromeUtils.unregisterWindowActor("ReportSiteIssueHelper");
+ resProto.setSubstitution("report-site-issue", null);
+ }
+};
+
+function getActorForBrowsingContext(name, browsingContext) {
+ const windowGlobal = browsingContext.currentWindowGlobal;
+ return windowGlobal ? windowGlobal.getActor(name) : null;
+}
+
+function gatherActors(name, browsingContext) {
+ const list = [];
+
+ const actor = getActorForBrowsingContext(name, browsingContext);
+ if (actor) {
+ list.push(actor);
+ }
+
+ for (const child of browsingContext.children) {
+ list.push(...gatherActors(name, child));
+ }
+
+ return list;
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json
new file mode 100644
index 0000000000..7769049798
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json
@@ -0,0 +1,21 @@
+[
+ {
+ "namespace": "tabExtras",
+ "description": "experimental tab API extensions",
+ "functions": [
+ {
+ "name": "getWebcompatInfo",
+ "type": "function",
+ "description": "Gets the content blocking status and script log for a given tab",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "tabId",
+ "minimum": 0
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/locales/en-US/webcompat.properties b/browser/extensions/report-site-issue/locales/en-US/webcompat.properties
new file mode 100644
index 0000000000..ee8cab2cf0
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/en-US/webcompat.properties
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE(wc-reporter.label2): This string will be used in the
+# Firefox page actions menu. Localized length should be considered.
+wc-reporter.label2=Report Site Issue…
+# LOCALIZATION NOTE(wc-reporter.tooltip): A site compatibility issue is
+# a website bug that exists in one browser (Firefox), but not another.
+wc-reporter.tooltip=Report a site compatibility issue
diff --git a/browser/extensions/report-site-issue/locales/jar.mn b/browser/extensions/report-site-issue/locales/jar.mn
new file mode 100644
index 0000000000..3422f6248f
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# 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/.
+
+[features/webcompat-reporter@mozilla.org] @AB_CD@.jar:
+% locale report-site-issue @AB_CD@ %locale/@AB_CD@/
+ locale/@AB_CD@/webcompat.properties (%webcompat.properties)
diff --git a/browser/extensions/report-site-issue/locales/moz.build b/browser/extensions/report-site-issue/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/extensions/report-site-issue/manifest.json b/browser/extensions/report-site-issue/manifest.json
new file mode 100644
index 0000000000..12cfd89988
--- /dev/null
+++ b/browser/extensions/report-site-issue/manifest.json
@@ -0,0 +1,66 @@
+{
+ "manifest_version": 2,
+ "name": "WebCompat Reporter",
+ "description": "Report site compatibility issues on webcompat.com",
+ "author": "Thomas Wisniewski <twisniewski@mozilla.com>",
+ "version": "1.5.1",
+ "homepage_url": "https://github.com/mozilla/webcompat-reporter",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "webcompat-reporter@mozilla.org"
+ }
+ },
+ "experiment_apis": {
+ "aboutConfigPrefs": {
+ "schema": "experimentalAPIs/aboutConfigPrefs.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/aboutConfigPrefs.js",
+ "paths": [["aboutConfigPrefs"]]
+ }
+ },
+ "browserInfo": {
+ "schema": "experimentalAPIs/browserInfo.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/browserInfo.js",
+ "paths": [["browserInfo"]]
+ }
+ },
+ "helpMenu": {
+ "schema": "experimentalAPIs/helpMenu.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/helpMenu.js",
+ "paths": [["helpMenu"]]
+ }
+ },
+ "l10n": {
+ "schema": "experimentalAPIs/l10n.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/l10n.js",
+ "paths": [["l10n"]]
+ }
+ },
+ "tabExtras": {
+ "schema": "experimentalAPIs/tabExtras.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/tabExtras.js",
+ "paths": [["tabExtras"]]
+ }
+ }
+ },
+ "icons": {
+ "16": "icons/lightbulb.svg",
+ "32": "icons/lightbulb.svg",
+ "48": "icons/lightbulb.svg",
+ "96": "icons/lightbulb.svg",
+ "128": "icons/lightbulb.svg"
+ },
+ "permissions": ["tabs", "<all_urls>"],
+ "background": {
+ "scripts": ["background.js"]
+ }
+}
diff --git a/browser/extensions/report-site-issue/moz.build b/browser/extensions/report-site-issue/moz.build
new file mode 100644
index 0000000000..b946897b0e
--- /dev/null
+++ b/browser/extensions/report-site-issue/moz.build
@@ -0,0 +1,41 @@
+# -*- 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/.
+
+DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"]
+DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"]
+
+DIRS += ["locales"]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"] += [
+ "background.js",
+ "manifest.json",
+]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"].experimentalAPIs += [
+ "experimentalAPIs/aboutConfigPrefs.js",
+ "experimentalAPIs/aboutConfigPrefs.json",
+ "experimentalAPIs/browserInfo.js",
+ "experimentalAPIs/browserInfo.json",
+ "experimentalAPIs/helpMenu.js",
+ "experimentalAPIs/helpMenu.json",
+ "experimentalAPIs/l10n.js",
+ "experimentalAPIs/l10n.json",
+ "experimentalAPIs/tabExtras.js",
+ "experimentalAPIs/tabExtras.json",
+]
+
+FINAL_TARGET_FILES.features[
+ "webcompat-reporter@mozilla.org"
+].experimentalAPIs.actors += ["experimentalAPIs/actors/tabExtrasActor.jsm"]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"].icons += [
+ "../../../toolkit/themes/shared/icons/lightbulb.svg"
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Web Compatibility", "Tooling & Investigations")
diff --git a/browser/extensions/report-site-issue/test/browser/browser.ini b/browser/extensions/report-site-issue/test/browser/browser.ini
new file mode 100644
index 0000000000..9c0b4c5968
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files =
+ frameworks.html
+ fastclick.html
+ head.js
+ test.html
+ webcompat.html
+
+[browser_button_state.js]
+skip-if = true # Disabled until we figure out why it is failing in bug 1775526
+[browser_disabled_cleanup.js]
+skip-if = true # Disabled until we figure out why it is failing in bug 1775526
+[browser_report_site_issue.js]
+skip-if = true # Disabled until we figure out why it is failing in bug 1775526
diff --git a/browser/extensions/report-site-issue/test/browser/browser_button_state.js b/browser/extensions/report-site-issue/test/browser/browser_button_state.js
new file mode 100644
index 0000000000..6111a26975
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_button_state.js
@@ -0,0 +1,52 @@
+"use strict";
+
+const REPORTABLE_PAGE = "http://example.com/";
+const REPORTABLE_PAGE2 = "https://example.com/";
+const NONREPORTABLE_PAGE = "about:mozilla";
+
+/* Test that the Report Site Issue help menu item is enabled for http and https tabs,
+ on page load, or TabSelect, and disabled for everything else. */
+add_task(async function test_button_state_disabled() {
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_WC_REPORTER_ENABLED, true]] });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE
+ );
+ const menu = new HelpMenuHelper();
+ await menu.open();
+ is(
+ menu.isItemEnabled(),
+ true,
+ "Check that panel item is enabled for reportable schemes on tab load"
+ );
+ await menu.close();
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NONREPORTABLE_PAGE
+ );
+ await menu.open();
+ is(
+ menu.isItemEnabled(),
+ false,
+ "Check that panel item is disabled for non-reportable schemes on tab load"
+ );
+ await menu.close();
+
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE2
+ );
+ await menu.open();
+ is(
+ menu.isItemEnabled(),
+ true,
+ "Check that panel item is enabled for reportable schemes on tab load"
+ );
+ await menu.close();
+
+ await BrowserTestUtils.removeTab(tab1);
+ await BrowserTestUtils.removeTab(tab2);
+ await BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js b/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js
new file mode 100644
index 0000000000..65b6ff7369
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js
@@ -0,0 +1,41 @@
+"use strict";
+
+// Test the addon is cleaning up after itself when disabled.
+add_task(async function test_disabled() {
+ await promiseAddonEnabled();
+
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com" },
+ async function () {
+ const menu = new HelpMenuHelper();
+ await menu.open();
+ is(
+ menu.isItemHidden(),
+ true,
+ "Report Site Issue help menu item is hidden."
+ );
+ await menu.close();
+ }
+ );
+
+ await promiseAddonEnabled();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com" },
+ async function () {
+ const menu = new HelpMenuHelper();
+ await menu.open();
+ is(
+ await menu.isItemHidden(),
+ false,
+ "Report Site Issue help menu item is visible."
+ );
+ await menu.close();
+ }
+ );
+
+ // Shut down the addon at the end,or the new instance started when we re-enabled it will "leak".
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+});
diff --git a/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js
new file mode 100644
index 0000000000..62d3516c46
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js
@@ -0,0 +1,300 @@
+"use strict";
+
+async function clickToReportAndAwaitReportTabLoad() {
+ const helpMenu = new HelpMenuHelper();
+ await helpMenu.open();
+
+ // click on "report site issue" and wait for the new tab to open
+ const tab = await new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ document.getElementById("help_reportSiteIssue").click();
+ });
+
+ // wait for the new tab to acknowledge that it received a screenshot
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "ScreenshotReceived",
+ false,
+ null,
+ true
+ );
+
+ await helpMenu.close();
+
+ return tab;
+}
+
+add_task(async function start_issue_server() {
+ requestLongerTimeout(2);
+
+ const serverLanding = await startIssueServer();
+
+ // ./head.js sets the value for PREF_WC_REPORTER_ENDPOINT
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["datareporting.healthreport.uploadEnabled", true],
+ [PREF_WC_REPORTER_ENABLED, true],
+ [PREF_WC_REPORTER_ENDPOINT, serverLanding],
+ ],
+ });
+});
+
+/* Test that clicking on the Report Site Issue button opens a new tab
+ and sends a postMessaged blob to it. */
+add_task(async function test_opened_page() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(
+ tab2.linkedBrowser,
+ [{ TEST_PAGE }],
+ async function (args) {
+ async function isGreen(dataUrl) {
+ const getPixel = await new Promise(resolve => {
+ const myCanvas = content.document.createElement("canvas");
+ const ctx = myCanvas.getContext("2d");
+ const img = new content.Image();
+ img.onload = () => {
+ ctx.drawImage(img, 0, 0);
+ resolve((x, y) => {
+ return ctx.getImageData(x, y, 1, 1).data;
+ });
+ };
+ img.src = dataUrl;
+ });
+ function isPixelGreenFuzzy(p) {
+ // jpeg, so it will be off slightly
+ const fuzz = 4;
+ return p[0] < fuzz && Math.abs(p[1] - 128) < fuzz && p[2] < fuzz;
+ }
+ ok(isPixelGreenFuzzy(getPixel(0, 0)), "The pixels were green");
+ }
+
+ let doc = content.document;
+ let urlParam = doc.getElementById("url").innerText;
+ let preview = doc.getElementById("screenshot-preview");
+ const URL =
+ "http://example.com/browser/browser/extensions/report-site-issue/test/browser/test.html";
+ is(
+ urlParam,
+ args.TEST_PAGE,
+ "Reported page is correctly added to the url param"
+ );
+
+ let docShell = content.docShell;
+ is(
+ typeof docShell.getHasTrackingContentBlocked,
+ "function",
+ "docShell.hasTrackingContentBlocked is available"
+ );
+
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ ok(Array.isArray(details.consoleLog), "Details has a consoleLog array.");
+
+ const log1 = details.consoleLog[0];
+ is(log1.log[0], null, "Can handle degenerate console logs");
+ is(log1.level, "log", "Reports correct log level");
+ is(log1.uri, URL, "Reports correct url");
+ is(log1.pos, "7:13", "Reports correct line and column");
+
+ const log2 = details.consoleLog[1];
+ is(log2.log[0], "colored message", "Can handle fancy console logs");
+ is(log2.level, "error", "Reports correct log level");
+ is(log2.uri, URL, "Reports correct url");
+ is(log2.pos, "8:13", "Reports correct line and column");
+
+ const log3 = details.consoleLog[2];
+ const loggedObject = log3.log[0];
+ is(loggedObject.testobj, "{...}", "Reports object inside object");
+ is(
+ loggedObject.testSymbol,
+ "Symbol(sym)",
+ "Reports symbol inside object"
+ );
+ is(loggedObject.testnumber, 1, "Reports number inside object");
+ is(loggedObject.testArray, "(4)[...]", "Reports array inside object");
+ is(loggedObject.testUndf, "undefined", "Reports undefined inside object");
+ is(loggedObject.testNull, null, "Reports null inside object");
+ is(
+ loggedObject.testFunc,
+ undefined,
+ "Reports function inside object as undefined due to security reasons"
+ );
+ is(loggedObject.testString, "string", "Reports string inside object");
+ is(loggedObject.c, "{...}", "Reports circular reference inside object");
+ is(
+ Object.keys(loggedObject).length,
+ 10,
+ "Preview has 10 keys inside object"
+ );
+ is(log3.level, "log", "Reports correct log level");
+ is(log3.uri, URL, "Reports correct url");
+ is(log3.pos, "24:13", "Reports correct line and column");
+
+ const log4 = details.consoleLog[3];
+ const loggedArray = log4.log[0];
+ is(loggedArray[0], "string", "Reports string inside array");
+ is(loggedArray[1], "{...}", "Reports object inside array");
+ is(loggedArray[2], null, "Reports null inside array");
+ is(loggedArray[3], 90, "Reports number inside array");
+ is(loggedArray[4], "undefined", "Reports undefined inside array");
+ is(
+ loggedArray[5],
+ "undefined",
+ "Reports function inside array as undefined due to security reasons"
+ );
+ is(loggedArray[6], "(4)[...]", "Reports array inside array");
+ is(loggedArray[7], "(8)[...]", "Reports circular array inside array");
+
+ const log5 = details.consoleLog[4];
+ ok(
+ log5.log[0].match(/TypeError: .*document\.access is undefined/),
+ "Script errors are logged"
+ );
+ is(log5.level, "error", "Reports correct log level");
+ is(log5.uri, URL, "Reports correct url");
+ is(log5.pos, "36:5", "Reports correct line and column");
+
+ ok(typeof details.buildID == "string", "Details has a buildID string.");
+ ok(typeof details.channel == "string", "Details has a channel string.");
+ ok(
+ typeof details.hasTouchScreen == "boolean",
+ "Details has a hasTouchScreen flag."
+ );
+ ok(
+ typeof details.hasFastClick == "undefined",
+ "Details does not have FastClick if not found."
+ );
+ ok(
+ typeof details.hasMobify == "undefined",
+ "Details does not have Mobify if not found."
+ );
+ ok(
+ typeof details.hasMarfeel == "undefined",
+ "Details does not have Marfeel if not found."
+ );
+ ok(
+ typeof details["mixed active content blocked"] == "boolean",
+ "Details has a mixed active content blocked flag."
+ );
+ ok(
+ typeof details["mixed passive content blocked"] == "boolean",
+ "Details has a mixed passive content blocked flag."
+ );
+ ok(
+ typeof details["tracking content blocked"] == "string",
+ "Details has a tracking content blocked string."
+ );
+ ok(
+ typeof details["gfx.webrender.all"] == "boolean",
+ "Details has gfx.webrender.all."
+ );
+ ok(
+ typeof details["gfx.webrender.blob-images"] == "boolean",
+ "Details has gfx.webrender.blob-images."
+ );
+ ok(
+ typeof details["gfx.webrender.enabled"] == "boolean",
+ "Details has gfx.webrender.enabled."
+ );
+ ok(
+ typeof details["image.mem.shared"] == "boolean",
+ "Details has image.mem.shared."
+ );
+
+ is(
+ preview.innerText,
+ "Pass",
+ "A Blob object was successfully transferred to the test page."
+ );
+
+ const bgUrl = preview.style.backgroundImage.match(/url\(\"(.*)\"\)/)[1];
+ ok(
+ bgUrl.startsWith("data:image/jpeg;base64,"),
+ "A jpeg screenshot was successfully postMessaged"
+ );
+ await isGreen(bgUrl);
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_framework_detection() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FRAMEWORKS_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) {
+ let doc = content.document;
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ is(details.hasFastClick, true, "FastClick was found.");
+ is(details.hasMobify, true, "Mobify was found.");
+ is(details.hasMarfeel, true, "Marfeel was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_fastclick_detection() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FASTCLICK_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) {
+ let doc = content.document;
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ is(details.hasFastClick, true, "FastClick was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_framework_label() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FRAMEWORKS_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) {
+ let doc = content.document;
+ let labelParam = doc.getElementById("label").innerText;
+ const label = JSON.parse(labelParam);
+ ok(typeof label == "object", "Label param is a stringified JSON object.");
+ is(label.includes("type-fastclick"), true, "FastClick was found.");
+ is(label.includes("type-mobify"), true, "Mobify was found.");
+ is(label.includes("type-marfeel"), true, "Marfeel was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/extensions/report-site-issue/test/browser/fastclick.html b/browser/extensions/report-site-issue/test/browser/fastclick.html
new file mode 100644
index 0000000000..e13329dfd7
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/fastclick.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ "use strict";
+ function ObscuredFastClick() {
+ }
+ ObscuredFastClick.prototype = {
+ needsClick: () => {},
+ };
+ window.someRandomVar = new ObscuredFastClick();
+</script>
diff --git a/browser/extensions/report-site-issue/test/browser/frameworks.html b/browser/extensions/report-site-issue/test/browser/frameworks.html
new file mode 100644
index 0000000000..14df387ec9
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/frameworks.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ "use strict";
+ function FastClick() {}
+ function marfeel() {}
+ var Mobify = {Tag: "something"};
+</script>
diff --git a/browser/extensions/report-site-issue/test/browser/head.js b/browser/extensions/report-site-issue/test/browser/head.js
new file mode 100644
index 0000000000..8fd4e91406
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/head.js
@@ -0,0 +1,119 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+);
+
+const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
+const PREF_WC_REPORTER_ENDPOINT =
+ "extensions.webcompat-reporter.newIssueEndpoint";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const TEST_PAGE = TEST_ROOT + "test.html";
+const FRAMEWORKS_TEST_PAGE = TEST_ROOT + "frameworks.html";
+const FASTCLICK_TEST_PAGE = TEST_ROOT + "fastclick.html";
+const NEW_ISSUE_PAGE = TEST_ROOT + "webcompat.html";
+
+const WC_ADDON_ID = "webcompat-reporter@mozilla.org";
+
+async function promiseAddonEnabled() {
+ const addon = await AddonManager.getAddonByID(WC_ADDON_ID);
+ if (addon.isActive) {
+ return;
+ }
+ const pref = SpecialPowers.Services.prefs.getBoolPref(
+ PREF_WC_REPORTER_ENABLED,
+ false
+ );
+ if (!pref) {
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, true);
+ }
+}
+
+class HelpMenuHelper {
+ #popup = null;
+
+ async open() {
+ this.popup = document.getElementById("menu_HelpPopup");
+ ok(this.popup, "Help menu should exist");
+
+ const menuOpen = BrowserTestUtils.waitForEvent(this.popup, "popupshown");
+
+ // This event-faking method was copied from browser_title_case_menus.js so
+ // this can be tested on MacOS (where the actual menus cannot be opened in
+ // tests, but we only need the help menu to populate itself).
+ this.popup.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true }));
+ this.popup.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
+
+ await menuOpen;
+ }
+
+ async close() {
+ if (this.popup) {
+ const menuClose = BrowserTestUtils.waitForEvent(
+ this.popup,
+ "popuphidden"
+ );
+
+ // (Also copied from browser_title_case_menus.js)
+ // Just for good measure, we'll fire the popuphiding/popuphidden events
+ // after we close the menupopups.
+ this.popup.dispatchEvent(
+ new MouseEvent("popuphiding", { bubbles: true })
+ );
+ this.popup.dispatchEvent(
+ new MouseEvent("popuphidden", { bubbles: true })
+ );
+
+ await menuClose;
+ this.popup = null;
+ }
+ }
+
+ isItemHidden() {
+ const item = document.getElementById("help_reportSiteIssue");
+ return item && item.hidden;
+ }
+
+ isItemEnabled() {
+ const item = document.getElementById("help_reportSiteIssue");
+ return item && !item.hidden && !item.disabled;
+ }
+}
+
+async function startIssueServer() {
+ const landingTemplate = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", NEW_ISSUE_PAGE);
+ xhr.onload = () => {
+ resolve(xhr.responseText);
+ };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+
+ const { HttpServer } = ChromeUtils.import(
+ "resource://testing-common/httpd.js"
+ );
+ const server = new HttpServer();
+
+ registerCleanupFunction(async function cleanup() {
+ await new Promise(resolve => server.stop(resolve));
+ });
+
+ server.registerPathHandler("/new", function (request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(landingTemplate);
+ });
+
+ server.start(-1);
+ return `http://localhost:${server.identity.primaryPort}/new`;
+}
diff --git a/browser/extensions/report-site-issue/test/browser/test.html b/browser/extensions/report-site-issue/test/browser/test.html
new file mode 100644
index 0000000000..ed1844f530
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ /* eslint-disable no-console */
+ /* eslint-disable no-unused-expressions */
+ "use strict";
+ console.log(null);
+ console.error("%ccolored message", "background:green; color:white");
+ const obj = {
+ testSymbol: Symbol("sym"),
+ testobj: {},
+ testnumber: 1,
+ testArray: [1, {}, 2, 555],
+ testUndf: undefined,
+ testNull: null,
+ testFunc() {},
+ testString: 'string',
+ prop1: 'prop1',
+ prop2: 'prop2'
+ };
+ obj.c = obj;
+ obj.prop3 = 'prop3';
+ obj.prop4 = 'prop4';
+ console.log(obj);
+ const arr = [
+ 'string',
+ {test: 'obj'},
+ null,
+ 90,
+ undefined,
+ function() {},
+ [1, {}, 2, 555]
+ ];
+ arr.push(arr);
+ console.log(arr);
+ document.access.non.existent.property.to.trigger.error;
+</script>
+<style>
+ body {background: rgb(0, 128, 0);}
+</style>
diff --git a/browser/extensions/report-site-issue/test/browser/webcompat.html b/browser/extensions/report-site-issue/test/browser/webcompat.html
new file mode 100644
index 0000000000..872c8917b7
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/webcompat.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ #screenshot-preview {width: 200px; height: 200px;}
+</style>
+<div id="url">$$URL$$</div>
+<div id="details">$$DETAILS$$</div>
+<div id="label">$$LABEL$$</div>
+<div id="screenshot-preview">Fail</div>
+<script>
+"use strict";
+let preview = document.getElementById("screenshot-preview");
+const CONFIG = {
+ url: {
+ element: document.getElementById("url")
+ },
+ details: {
+ element: document.getElementById("details"),
+ toStringify: true
+ },
+ extra_labels: {
+ element: document.getElementById("label"),
+ toStringify: true
+ },
+};
+
+function getBlobAsDataURL(blob) {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ reader.addEventListener("error", (e) => {
+ reject(`There was an error reading the blob: ${e.type}`);
+ });
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ reader.addEventListener("load", (e) => {
+ resolve(e.target.result);
+ });
+
+ reader.readAsDataURL(blob);
+ });
+}
+
+function setPreviewBG(backgroundData) {
+ return new Promise((resolve) => {
+ preview.style.background = `url(${backgroundData})`;
+ resolve();
+ });
+}
+
+function sendReceivedEvent() {
+ window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles: true}));
+}
+
+function prepareContent(toStringify, content) {
+ if (toStringify) {
+ return JSON.stringify(content)
+ }
+
+ return content;
+}
+
+function appendMessage(message) {
+ for (const key in CONFIG) {
+ if (key in message) {
+ const field = CONFIG[key];
+ field.element.innerText = prepareContent(field.toStringify, message[key]);
+ }
+ }
+}
+
+// eslint-disable-next-line mozilla/balanced-listeners
+window.addEventListener("message", function(event) {
+ if (event.data.screenshot instanceof Blob) {
+ preview.innerText = "Pass";
+ }
+
+ if (event.data.message) {
+ appendMessage(event.data.message);
+ }
+
+ getBlobAsDataURL(event.data.screenshot).then(setPreviewBG).then(sendReceivedEvent);
+});
+</script>