summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/browser/head_abuse_report.js')
-rw-r--r--toolkit/mozapps/extensions/test/browser/head_abuse_report.js587
1 files changed, 587 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
new file mode 100644
index 0000000000..89daa2a219
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
@@ -0,0 +1,587 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+/* exported installTestExtension, addCommonAbuseReportTestTasks,
+ * createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID,
+ * gManagerWindow, handleSubmitRequest, makeWidgetId,
+ * waitForNewWindow, waitClosedWindow, AbuseReporter,
+ * AbuseReporterTestUtils, AddonTestUtils
+ */
+
+/* global MockProvider, loadInitialView, closeView */
+
+const { AbuseReporter } = ChromeUtils.importESModule(
+ "resource://gre/modules/AbuseReporter.sys.mjs"
+);
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+);
+
+const { makeWidgetId } = ExtensionCommon;
+
+const ADDON_ID = "test-extension-to-report@mochi.test";
+const REPORT_ENTRY_POINT = "menu";
+const BASE_TEST_MANIFEST = {
+ name: "Fake extension to report",
+ author: "Fake author",
+ homepage_url: "https://fake.extension.url/",
+};
+const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org";
+const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
+const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
+const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test";
+const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test";
+const EXT_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test";
+const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test";
+
+let gManagerWindow;
+
+AddonTestUtils.initMochitest(this);
+
+async function openAboutAddons(type = "extension") {
+ gManagerWindow = await loadInitialView(type);
+}
+
+async function closeAboutAddons() {
+ if (gManagerWindow) {
+ await closeView(gManagerWindow);
+ gManagerWindow = null;
+ }
+}
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ let listener = win => {
+ Services.obs.removeObserver(listener, "toplevel-window-ready");
+ resolve(win);
+ };
+
+ Services.obs.addObserver(listener, "toplevel-window-ready");
+ });
+}
+
+function waitClosedWindow(win) {
+ return new Promise((resolve, reject) => {
+ function onWindowClosed() {
+ if (win && !win.closed) {
+ // If a specific window reference has been passed, then check
+ // that the window is closed before resolving the promise.
+ return;
+ }
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ resolve();
+ }
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+}
+
+async function installTestExtension(
+ id = ADDON_ID,
+ type = "extension",
+ manifest = {}
+) {
+ let additionalProps = {
+ icons: {
+ 32: "test-icon.png",
+ },
+ };
+
+ switch (type) {
+ case "theme":
+ additionalProps = {
+ ...additionalProps,
+ theme: {
+ colors: {
+ frame: "#a14040",
+ tab_background_text: "#fac96e",
+ },
+ },
+ };
+ break;
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
+ // implementation is also removed.
+ case "sitepermission-deprecated":
+ additionalProps = {
+ name: "WebMIDI test addon for https://mochi.test",
+ install_origins: ["https://mochi.test"],
+ site_permissions: ["midi"],
+ };
+ break;
+ case "extension":
+ break;
+ default:
+ throw new Error(`Unexpected addon type: ${type}`);
+ }
+
+ const extensionOpts = {
+ manifest: {
+ ...BASE_TEST_MANIFEST,
+ ...additionalProps,
+ ...manifest,
+ browser_specific_settings: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ };
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
+ // implementation is also removed.
+ if (type === "sitepermission-deprecated") {
+ const xpi = AddonTestUtils.createTempWebExtensionFile(extensionOpts);
+ const addon = await AddonManager.installTemporaryAddon(xpi);
+ // The extension object that ExtensionTestUtils.loadExtension returns for
+ // mochitest is pretty tight to the Extension class, and so for now this
+ // returns a more minimal `extension` test object which only provides the
+ // `unload` method.
+ //
+ // For the purpose of the abuse reports tests that are using this helper
+ // this should be already enough.
+ return {
+ addon,
+ unload: () => addon.uninstall(),
+ };
+ }
+
+ const extension = ExtensionTestUtils.loadExtension(extensionOpts);
+ await extension.startup();
+ return extension;
+}
+
+function handleSubmitRequest({ request, response }) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.write("{}");
+}
+
+const AbuseReportTestUtils = {
+ _mockProvider: null,
+ _mockServer: null,
+ _abuseRequestHandlers: [],
+
+ // Mock addon details API endpoint.
+ amoAddonDetailsMap: new Map(),
+
+ // Setup the test environment by setting the expected prefs and
+ // initializing MockProvider and the mock AMO server.
+ async setup() {
+ // Enable html about:addons and the abuse reporting and
+ // set the api endpoints url to the mock service.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.abuseReport.enabled", true],
+ ["extensions.abuseReport.url", "http://test.addons.org/api/report/"],
+ [
+ "extensions.abuseReport.amoDetailsURL",
+ "http://test.addons.org/api/addons/addon/",
+ ],
+ ],
+ });
+
+ this._setupMockProvider();
+ this._setupMockServer();
+ },
+
+ // Returns the currently open abuse report dialog window (if any).
+ getReportDialog() {
+ return Services.ww.getWindowByName("addons-abuse-report-dialog");
+ },
+
+ // Returns the parameters related to the report dialog (if any).
+ getReportDialogParams() {
+ const win = this.getReportDialog();
+ return win && win.arguments[0] && win.arguments[0].wrappedJSObject;
+ },
+
+ // Returns a reference to the addon-abuse-report element from the currently
+ // open abuse report.
+ getReportPanel() {
+ const win = this.getReportDialog();
+ ok(win, "Got an abuse report dialog open");
+ return win && win.document.querySelector("addon-abuse-report");
+ },
+
+ // Returns the list of abuse report reasons.
+ getReasons(abuseReportEl) {
+ return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS);
+ },
+
+ // Returns the info related to a given abuse report reason.
+ getReasonInfo(abuseReportEl, reason) {
+ return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason];
+ },
+
+ async promiseReportOpened({ addonId, reportEntryPoint, managerWindow }) {
+ let abuseReportEl;
+
+ if (!this.getReportDialog()) {
+ info("Wait for the report dialog window");
+ const dialog = await waitForNewWindow();
+ is(dialog, this.getReportDialog(), "Report dialog opened");
+ }
+
+ info("Wait for the abuse report panel render");
+ abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ ok(abuseReportEl, "Got an abuse report panel");
+ is(
+ abuseReportEl.addon && abuseReportEl.addon.id,
+ addonId,
+ "Abuse Report panel rendered for the expected addonId"
+ );
+ is(
+ abuseReportEl._report && abuseReportEl._report.reportEntryPoint,
+ reportEntryPoint,
+ "Abuse Report panel rendered for the expected reportEntryPoint"
+ );
+
+ return abuseReportEl;
+ },
+
+ // Return a promise resolved when the currently open report panel
+ // is closed.
+ // Also asserts that a specific report panel element has been closed,
+ // if one has been provided through the optional panel parameter.
+ async promiseReportClosed(panel) {
+ const win = panel ? panel.ownerGlobal : this.getReportDialog();
+ if (!win || win.closed) {
+ throw Error("Expected report dialog not found or already closed");
+ }
+
+ await waitClosedWindow(win);
+ // Assert that the panel has been closed (if the caller has passed it).
+ if (panel) {
+ ok(!panel.ownerGlobal, "abuse report dialog closed");
+ }
+ },
+
+ // Returns a promise resolved when the report panel has been rendered
+ // (rejects is there is no dialog currently open).
+ async promiseReportDialogRendered() {
+ const params = this.getReportDialogParams();
+ if (!params) {
+ throw new Error("abuse report dialog not found");
+ }
+ return params.promiseReportPanel;
+ },
+
+ // Given a `requestHandler` function, an HTTP server handler function
+ // to use to handle a report submit request received by the mock AMO server),
+ // returns a promise resolved when the mock AMO server has received and
+ // handled the report submit request.
+ async promiseReportSubmitHandled(requestHandler) {
+ if (typeof requestHandler !== "function") {
+ throw new Error("requestHandler should be a function");
+ }
+ return new Promise((resolve, reject) => {
+ this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler });
+ });
+ },
+
+ // Return a promise resolved to the abuse report panel element,
+ // once its rendering is completed.
+ // If abuseReportEl is undefined, it looks for the currently opened
+ // report panel.
+ async promiseReportRendered(abuseReportEl) {
+ let el = abuseReportEl;
+
+ if (!el) {
+ const win = this.getReportDialog();
+ if (!win) {
+ await waitForNewWindow();
+ }
+
+ el = await this.promiseReportDialogRendered();
+ ok(el, "Got an abuse report panel");
+ }
+
+ return el._radioCheckedReason
+ ? el
+ : BrowserTestUtils.waitForEvent(
+ el,
+ "abuse-report:updated",
+ "Wait the abuse report panel to be rendered"
+ ).then(() => el);
+ },
+
+ // A promise resolved when the given abuse report panel element
+ // has been rendered. If a panel name ("reasons" or "submit") is
+ // passed as a second parameter, it also asserts that the panel is
+ // updated to the expected view mode.
+ async promiseReportUpdated(abuseReportEl, panel) {
+ const evt = await BrowserTestUtils.waitForEvent(
+ abuseReportEl,
+ "abuse-report:updated",
+ "Wait abuse report panel update"
+ );
+
+ if (panel) {
+ is(evt.detail.panel, panel, `Got a "${panel}" update event`);
+
+ const el = abuseReportEl;
+ switch (evt.detail.panel) {
+ case "reasons":
+ ok(!el._reasonsPanel.hidden, "Reasons panel should be visible");
+ ok(el._submitPanel.hidden, "Submit panel should be hidden");
+ break;
+ case "submit":
+ ok(el._reasonsPanel.hidden, "Reasons panel should be hidden");
+ ok(!el._submitPanel.hidden, "Submit panel should be visible");
+ break;
+ }
+ }
+ },
+
+ // Returns a promise resolved once the expected number of abuse report
+ // message bars have been created.
+ promiseMessageBars(expectedMessageBarCount) {
+ return new Promise(resolve => {
+ const details = [];
+ function listener(evt) {
+ details.push(evt.detail);
+ if (details.length >= expectedMessageBarCount) {
+ cleanup();
+ resolve(details);
+ }
+ }
+ function cleanup() {
+ if (gManagerWindow) {
+ gManagerWindow.document.removeEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ }
+ }
+ gManagerWindow.document.addEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ });
+ },
+
+ async assertFluentStrings(containerEl) {
+ // Make sure all localized elements have defined Fluent strings.
+ const localizedEls = Array.from(
+ containerEl.querySelectorAll("[data-l10n-id]")
+ );
+ ok(localizedEls.length, "Got localized elements");
+ for (let el of localizedEls) {
+ const l10nId = el.getAttribute("data-l10n-id");
+ await TestUtils.waitForCondition(
+ () => el.textContent !== "",
+ `Element with Fluent id '${l10nId}' should not be empty`
+ );
+ }
+ },
+
+ // Assert that the report action is hidden on the addon card
+ // for the given about:addons windows and extension id.
+ async assertReportActionHidden(gManagerWindow, extId) {
+ let addonCard = gManagerWindow.document.querySelector(
+ `addon-list addon-card[addon-id="${extId}"]`
+ );
+ ok(addonCard, `Got the addon-card for the ${extId} test extension`);
+
+ let reportButton = addonCard.querySelector("[action=report]");
+ ok(reportButton, `Got the report action for ${extId}`);
+ ok(reportButton.hidden, `${extId} report action should be hidden`);
+ },
+
+ // Assert that the report panel is hidden (or closed if the report
+ // panel is opened in its own dialog window).
+ async assertReportPanelHidden() {
+ const win = this.getReportDialog();
+ ok(!win, "Abuse Report dialog should be initially hidden");
+ },
+
+ createMockAddons(mockProviderAddons) {
+ this._mockProvider.createAddons(mockProviderAddons);
+ },
+
+ async clickPanelButton(buttonEl, { label = undefined } = {}) {
+ info(`Clicking the '${buttonEl.textContent.trim() || label}' button`);
+ // NOTE: ideally this should synthesize the mouse event,
+ // we call the click method to prevent intermittent timeouts
+ // due to the mouse event not received by the target element.
+ buttonEl.click();
+ },
+
+ triggerNewReport(addonId, reportEntryPoint) {
+ gManagerWindow.openAbuseReport({ addonId, reportEntryPoint });
+ },
+
+ triggerSubmit(reason, message) {
+ const reportEl =
+ this.getReportDialog().document.querySelector("addon-abuse-report");
+ reportEl._form.elements.message.value = message;
+ reportEl._form.elements.reason.value = reason;
+ reportEl.submit();
+ },
+
+ async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) {
+ // Close the current about:addons window if it has been leaved open from
+ // a previous test case failure.
+ if (gManagerWindow) {
+ await closeAboutAddons();
+ }
+
+ await openAboutAddons();
+
+ let promiseReportPanel = waitForNewWindow().then(() =>
+ this.promiseReportDialogRendered()
+ );
+
+ this.triggerNewReport(addonId, reportEntryPoint);
+
+ const panelEl = await promiseReportPanel;
+ await this.promiseReportRendered(panelEl);
+ is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`);
+
+ return panelEl;
+ },
+
+ async closeReportPanel(panelEl) {
+ const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl);
+
+ info("Cancel report and wait the dialog to be closed");
+ panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel"));
+
+ await onceReportClosed;
+ },
+
+ // Internal helper methods.
+
+ _setupMockProvider() {
+ this._mockProvider = new MockProvider();
+ this._mockProvider.createAddons([
+ {
+ id: THEME_NO_UNINSTALL_ID,
+ name: "This theme cannot be uninstalled",
+ version: "1.1",
+ creator: { name: "Theme creator", url: "http://example.com/creator" },
+ type: "theme",
+ permissions: 0,
+ },
+ {
+ id: EXT_WITH_PRIVILEGED_URL_ID,
+ name: "This extension has an unexpected privileged creator URL",
+ version: "1.1",
+ creator: { name: "creator", url: "about:config" },
+ type: "extension",
+ },
+ {
+ id: EXT_SYSTEM_ADDON_ID,
+ name: "This is a system addon",
+ version: "1.1",
+ creator: { name: "creator", url: "http://example.com/creator" },
+ type: "extension",
+ isSystem: true,
+ },
+ {
+ id: EXT_UNSUPPORTED_TYPE_ADDON_ID,
+ name: "This is a fake unsupported addon type",
+ version: "1.1",
+ type: "unsupported_addon_type",
+ },
+ {
+ id: EXT_LANGPACK_ADDON_ID,
+ name: "This is a fake langpack",
+ version: "1.1",
+ type: "locale",
+ },
+ {
+ id: EXT_DICTIONARY_ADDON_ID,
+ name: "This is a fake dictionary",
+ version: "1.1",
+ type: "dictionary",
+ },
+ ]);
+ },
+
+ _setupMockServer() {
+ if (this._mockServer) {
+ return;
+ }
+
+ // Init test report api server.
+ const server = AddonTestUtils.createHttpServer({
+ hosts: ["test.addons.org"],
+ });
+ this._mockServer = server;
+
+ server.registerPathHandler("/api/report/", (request, response) => {
+ const stream = request.bodyInputStream;
+ const buffer = NetUtil.readInputStream(stream, stream.available());
+ const data = new TextDecoder().decode(buffer);
+ const promisedHandler = this._abuseRequestHandlers.pop();
+ if (promisedHandler) {
+ const { requestHandler, resolve, reject } = promisedHandler;
+ try {
+ requestHandler({ data, request, response });
+ resolve();
+ } catch (err) {
+ ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`);
+ reject(err);
+ }
+ } else {
+ ok(false, `Unexpected request: ${request.path} ${data}`);
+ }
+ });
+
+ server.registerPrefixHandler("/api/addons/addon/", (request, response) => {
+ const addonId = request.path.split("/").pop();
+ if (!this.amoAddonDetailsMap.has(addonId)) {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.write(JSON.stringify({ detail: "Not found." }));
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId)));
+ }
+ });
+ server.registerPathHandler(
+ "/assets/fake-icon-url.png",
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write("");
+ response.finish();
+ }
+ );
+ },
+};
+
+function createPromptConfirmEx({
+ remove = false,
+ report = false,
+ expectCheckboxHidden = false,
+} = {}) {
+ return (...args) => {
+ const checkboxState = args.pop();
+ const checkboxMessage = args.pop();
+ is(
+ checkboxState && checkboxState.value,
+ false,
+ "checkboxState should be initially false"
+ );
+ if (expectCheckboxHidden) {
+ ok(
+ !checkboxMessage,
+ "Should not have a checkboxMessage in promptService.confirmEx call"
+ );
+ } else {
+ ok(
+ checkboxMessage,
+ "Got a checkboxMessage in promptService.confirmEx call"
+ );
+ }
+
+ // Report checkbox selected.
+ checkboxState.value = report;
+
+ // Remove accepted.
+ return remove ? 0 : 1;
+ };
+}