diff options
Diffstat (limited to 'browser/extensions/report-site-issue')
29 files changed, 1880 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..e26bd6da3f --- /dev/null +++ b/browser/extensions/report-site-issue/.eslintrc.js @@ -0,0 +1,76 @@ +/* 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", + + "valid-jsdoc": [ + "error", + { + prefer: { + return: "returns", + }, + preferType: { + Boolean: "boolean", + Number: "number", + String: "string", + bool: "boolean", + }, + requireParamDescription: false, + requireReturn: false, + requireReturnDescription: false, + }, + ], + + // 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..d5c4edcbd7 --- /dev/null +++ b/browser/extensions/report-site-issue/background.js @@ -0,0 +1,215 @@ +/* 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.pageActionExtras.setLabelForHistogram("webcompat"); + +browser.pageAction.onClicked.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), + 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, + frameworks, + screenshot, + ]) => { + if (channel !== "linux") { + delete graphicsPrefs["layers.acceleration.force-enabled"]; + } + + const consoleLog = frameInfo.log; + delete frameInfo.log; + + return Object.assign(frameInfo, { + tabId: id, + blockList, + details: Object.assign(graphicsPrefs, { + buildID, + channel, + consoleLog, + frameworks, + 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, ""); +} + +browser.l10n + .getMessage("wc-reporter.label2") + .then(browser.pageActionExtras.setDefaultTitle, () => {}); + +browser.l10n + .getMessage("wc-reporter.tooltip") + .then(browser.pageActionExtras.setTooltipText, () => {}); + +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..97206ba754 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js @@ -0,0 +1,41 @@ +/* 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 */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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..869856dbea --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm @@ -0,0 +1,156 @@ +/* -*- 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"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var EXPORTED_SYMBOLS = ["ReportSiteIssueHelperChild"]; + +const PREVIEW_MAX_ITEMS = 10; +const LOG_LEVELS = ["debug", "info", "warn", "error"]; + +function getPreview(value) { + switch (typeof value) { + case "function": + return "function ()"; + + case "object": + if (value === null) { + return null; + } + + if (Array.isArray(value)) { + return `(${value.length})[...]`; + } + + return "{...}"; + + case "undefined": + return "undefined"; + + default: + 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..23d224dd98 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js @@ -0,0 +1,74 @@ +/* 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 */ + +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function isTelemetryEnabled() { + return Services.prefs.getBoolPref( + "datareporting.healthreport.uploadEnabled", + false + ); +} + +function isWebRenderEnabled() { + return ( + Services.prefs.getBoolPref("gfx.webrender.all", false) || + Services.prefs.getBoolPref("gfx.webrender.enabled", false) + ); +} + +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; + }, + }, + }; + } +}; 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..dc437c9bc9 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json @@ -0,0 +1,57 @@ +[ + { + "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 + } + ] + } +] 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..e87569562c --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js @@ -0,0 +1,57 @@ +/* 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 */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 { + Cu.reportError( + "Cannot find webcompat reporter chrome.manifest for registring 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..fd10cf68e7 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.json @@ -0,0 +1,19 @@ +[ + { + "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/pageActionExtras.js b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js new file mode 100644 index 0000000000..19ee738a60 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js @@ -0,0 +1,43 @@ +/* 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 */ + +this.pageActionExtras = class extends ExtensionAPI { + getAPI(context) { + const extension = context.extension; + const pageActionAPI = extension.apiManager.getAPI( + "pageAction", + extension, + context.envType + ); + const { + Management: { + global: { windowTracker }, + }, + } = ChromeUtils.import("resource://gre/modules/Extension.jsm", null); + return { + pageActionExtras: { + async setDefaultTitle(title) { + pageActionAPI.action.getContextData(null).title = title; + // Make sure the new default title is considered right away + for (const window of windowTracker.browserWindows()) { + const tab = window.gBrowser.selectedTab; + if (pageActionAPI.action.isShownForTab(tab)) { + pageActionAPI.updateButton(window); + } + } + }, + async setLabelForHistogram(label) { + pageActionAPI.browserPageAction._labelForHistogram = label; + }, + async setTooltipText(text) { + pageActionAPI.browserPageAction.setTooltip(text); + }, + }, + }; + } +}; diff --git a/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json new file mode 100644 index 0000000000..df848e439a --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json @@ -0,0 +1,41 @@ +[ + { + "namespace": "pageActionExtras", + "description": "experimental pageAction API extensions", + "functions": [ + { + "name": "setDefaultTitle", + "type": "function", + "async": true, + "description": "Set the page action's title for all tabs", + "parameters": [{ + "name": "title", + "type": "string", + "description": "title" + }] + }, + { + "name": "setLabelForHistogram", + "type": "function", + "async": true, + "description": "Set the page action's label for telemetry histograms", + "parameters": [{ + "name": "label", + "type": "string", + "description": "label for the histogram" + }] + }, + { + "name": "setTooltipText", + "type": "function", + "async": true, + "description": "Set the page action's tooltip text", + "parameters": [{ + "name": "text", + "type": "string", + "description": "text" + }] + } + ] + } +] 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..7da789f905 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js @@ -0,0 +1,97 @@ +/* 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 */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 { browsingContext } = tabManager.get(tabId).browser; + 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.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..d59230b2f6 --- /dev/null +++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json @@ -0,0 +1,19 @@ +[ + { + "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/icons/lightbulb.svg b/browser/extensions/report-site-issue/icons/lightbulb.svg new file mode 100644 index 0000000000..1f1e34da4f --- /dev/null +++ b/browser/extensions/report-site-issue/icons/lightbulb.svg @@ -0,0 +1,6 @@ +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="M8 0C4.3 0 2 2.107 2 5.5c0 2.372 2.065 4.268 3 5V14c0 1.476 1.616 2 3 2s3-.524 3-2v-3.5c.935-.736 3-2.632 3-5C14 2.107 11.7 0 8 0zm1 12H7v-1h2zm-1 2a3.086 3.086 0 0 1-1-.172V13h2v.828A3.047 3.047 0 0 1 8 14zm1.445-4.832A1 1 0 0 0 9 10H7a1 1 0 0 0-.444-.831C5.845 8.691 4 7.1 4 5.5 4 2.607 6.175 2 8 2s4 .607 4 3.5c0 1.6-1.845 3.191-2.555 3.668z"/> +</svg> 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..d0afa9e795 --- /dev/null +++ b/browser/extensions/report-site-issue/manifest.json @@ -0,0 +1,78 @@ +{ + "manifest_version": 2, + "name": "WebCompat Reporter", + "description": "Report site compatibility issues on webcompat.com", + "author": "Thomas Wisniewski <twisniewski@mozilla.com>", + "version": "1.4.0", + "homepage_url": "https://github.com/mozilla/webcompat-reporter", + "applications": { + "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"]] + } + }, + "l10n": { + "schema": "experimentalAPIs/l10n.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experimentalAPIs/l10n.js", + "paths": [["l10n"]] + } + }, + "pageActionExtras": { + "schema": "experimentalAPIs/pageActionExtras.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experimentalAPIs/pageActionExtras.js", + "paths": [["pageActionExtras"]] + } + }, + "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" + ] + }, + "page_action": { + "browser_style": true, + "default_icon": "icons/lightbulb.svg", + "default_title": "Report Site Issue…", + "pinned": false, + "show_matches": ["http://*/*", "https://*/*"] + } +} diff --git a/browser/extensions/report-site-issue/moz.build b/browser/extensions/report-site-issue/moz.build new file mode 100644 index 0000000000..e3e92a3319 --- /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/l10n.js", + "experimentalAPIs/l10n.json", + "experimentalAPIs/pageActionExtras.js", + "experimentalAPIs/pageActionExtras.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 += [ + "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/.eslintrc.js b/browser/extensions/report-site-issue/test/browser/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; 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..39861d8cc6 --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + frameworks.html + fastclick.html + head.js + test.html + webcompat.html + +[browser_disabled_cleanup.js] +[browser_button_state.js] +[browser_report_site_issue.js] 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..d97a8460d8 --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/browser_button_state.js @@ -0,0 +1,93 @@ +"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 panel 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 + ); + await openPageActions(); + is( + await isPanelItemEnabled(), + true, + "Check that panel item is enabled for reportable schemes on tab load" + ); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + NONREPORTABLE_PAGE + ); + await openPageActions(); + is( + await isPanelItemDisabled(), + true, + "Check that panel item is disabled for non-reportable schemes on tab load" + ); + + let tab3 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + REPORTABLE_PAGE2 + ); + await openPageActions(); + is( + await isPanelItemEnabled(), + true, + "Check that panel item is enabled for reportable schemes on tab load" + ); + + await BrowserTestUtils.removeTab(tab1); + await BrowserTestUtils.removeTab(tab2); + await BrowserTestUtils.removeTab(tab3); +}); + +/* Test that the button is enabled or disabled when we expected it to be, when + pinned to the URL bar. */ +add_task(async function test_button_state_in_urlbar() { + await SpecialPowers.pushPrefEnv({ set: [[PREF_WC_REPORTER_ENABLED, true]] }); + + pinToURLBar(); + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + REPORTABLE_PAGE + ); + await openPageActions(); + is( + await isURLButtonPresent(), + true, + "Check that urlbar icon is enabled for reportable schemes on tab load" + ); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + NONREPORTABLE_PAGE + ); + await openPageActions(); + is( + await isURLButtonPresent(), + false, + "Check that urlbar icon is hidden for non-reportable schemes on tab load" + ); + + let tab3 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + REPORTABLE_PAGE2 + ); + await openPageActions(); + is( + await isURLButtonPresent(), + true, + "Check that urlbar icon is enabled for reportable schemes on tab load" + ); + + unpinFromURLBar(); + 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..fac7a5022e --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js @@ -0,0 +1,53 @@ +"use strict"; + +// Test the addon is cleaning up after itself when disabled. +add_task(async function test_disabled() { + await promiseAddonEnabled(); + + pinToURLBar(); + + SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false); + await promisePageActionRemoved(); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http://example.com" }, + async function() { + await openPageActions(); + is( + await isPanelItemPresent(), + false, + "Report Site Issue button is not shown on the popup panel." + ); + is( + await isURLButtonPresent(), + false, + "Report Site Issue is not shown on the url bar." + ); + } + ); + + await promiseAddonEnabled(); + + pinToURLBar(); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http://example.com" }, + async function() { + await openPageActions(); + is( + await isPanelItemEnabled(), + true, + "Report Site Issue button is shown on the popup panel." + ); + is( + await isURLButtonPresent(), + true, + "Report Site Issue is shown on the url bar." + ); + } + ); + + // 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); + await promisePageActionRemoved(); +}); 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..f3d34990ba --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js @@ -0,0 +1,291 @@ +"use strict"; + +async function clickToReportAndAwaitReportTabLoad() { + await openPageActions(); + await isPanelItemEnabled(); + + // 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(WC_PAGE_ACTION_PANEL_ID).click(); + }); + + // wait for the new tab to acknowledge that it received a screenshot + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "ScreenshotReceived", + false, + null, + true + ); + + 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]; + ok(log1.log[0] === null, "Can handle degenerate console logs"); + ok(log1.level === "log", "Reports correct log level"); + ok(log1.uri === URL, "Reports correct url"); + ok(log1.pos === "7:13", "Reports correct line and column"); + + const log2 = details.consoleLog[1]; + ok(log2.log[0] === "colored message", "Can handle fancy console logs"); + ok(log2.level === "error", "Reports correct log level"); + ok(log2.uri === URL, "Reports correct url"); + ok(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.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" + ); + ok(log3.level === "log", "Reports correct log level"); + ok(log3.uri === URL, "Reports correct url"); + ok(log3.pos === "23: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" + ); + ok(log5.level === "error", "Reports correct log level"); + ok(log5.uri === URL, "Reports correct url"); + ok(log5.pos === "35: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..2779e7dfbb --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/head.js @@ -0,0 +1,204 @@ +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); + +const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm", + null +); + +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"; + +const WC_PAGE_ACTION_ID = "webcompat-reporter_mozilla_org"; +const WC_PAGE_ACTION_PANEL_ID = + "pageAction-panel-webcompat-reporter_mozilla_org"; +const WC_PAGE_ACTION_URLBAR_ID = + "pageAction-urlbar-webcompat-reporter_mozilla_org"; + +const oldPAadd = PageActions.addAction; +const placedSignal = "webext-page-action-placed"; +PageActions.addAction = function(action) { + oldPAadd.call(this, action); + if (action.id === WC_PAGE_ACTION_ID) { + Services.obs.notifyObservers(null, placedSignal); + } + return action; +}; +const oldPAremoved = PageActions.onActionRemoved; +const removedSignal = "webext-page-action-removed"; +PageActions.onActionRemoved = function(action) { + oldPAremoved.call(this, action); + if (action.id === WC_PAGE_ACTION_ID) { + Services.obs.notifyObservers(null, removedSignal); + } + return action; +}; + +function promisePageActionSignal(signal) { + return new Promise(done => { + const obs = function() { + Services.obs.removeObserver(obs, signal); + done(); + }; + Services.obs.addObserver(obs, signal); + }); +} + +function promisePageActionPlaced() { + return promisePageActionSignal(placedSignal); +} + +function promisePageActionRemoved() { + return promisePageActionSignal(removedSignal); +} + +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); + } + await promisePageActionPlaced(); +} + +async function isPanelItemEnabled() { + const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID); + return icon && !icon.disabled; +} + +async function isPanelItemDisabled() { + const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID); + return icon && icon.disabled; +} + +async function isPanelItemPresent() { + return document.getElementById(WC_PAGE_ACTION_PANEL_ID) !== null; +} + +async function isURLButtonPresent() { + return document.getElementById(WC_PAGE_ACTION_URLBAR_ID) !== null; +} + +function openPageActions() { + let dwu = window.windowUtils; + return BrowserTestUtils.waitForCondition(() => { + // Wait for the main page action button to become visible. It's hidden for + // some URIs, so depending on when this is called, it may not yet be quite + // visible. It's up to the caller to make sure it will be visible. + info("Waiting for main page action button to have non-0 size"); + let bounds = dwu.getBoundsWithoutFlushing( + BrowserPageActions.mainButtonNode + ); + return bounds.width > 0 && bounds.height > 0; + }) + .then(() => { + // Wait for the panel to become open, by clicking the button if necessary. + info("Waiting for main page action panel to be open"); + if (BrowserPageActions.panelNode.state == "open") { + return Promise.resolve(); + } + let shownPromise = promisePageActionPanelShown(); + EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {}); + return shownPromise; + }) + .then(() => { + // Wait for items in the panel to become visible. + return promisePageActionViewChildrenVisible( + BrowserPageActions.mainViewNode + ); + }); +} + +function promisePageActionPanelShown() { + return new Promise(resolve => { + if (BrowserPageActions.panelNode.state == "open") { + executeSoon(resolve); + return; + } + BrowserPageActions.panelNode.addEventListener( + "popupshown", + () => { + executeSoon(resolve); + }, + { once: true } + ); + }); +} + +function promisePageActionViewChildrenVisible(panelViewNode) { + info( + "promisePageActionViewChildrenVisible waiting for a child node to be visible" + ); + let dwu = window.windowUtils; + return BrowserTestUtils.waitForCondition(() => { + let bodyNode = panelViewNode.firstElementChild; + for (let childNode of bodyNode.children) { + let bounds = dwu.getBoundsWithoutFlushing(childNode); + if (bounds.width > 0 && bounds.height > 0) { + return true; + } + } + return false; + }); +} + +function pinToURLBar() { + PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = true; +} + +function unpinFromURLBar() { + PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = false; +} + +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..e2bae1d80a --- /dev/null +++ b/browser/extensions/report-site-issue/test/browser/test.html @@ -0,0 +1,39 @@ +<!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 = { + 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> |