Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
);
const { AddonTestUtils } = ChromeUtils.importESModule(
);
const { ExtensionCommon } = ChromeUtils.importESModule(
);
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 => {
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",
],
],
});
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 }) {
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.
let localizedEls = Array.from(
containerEl.querySelectorAll("[data-l10n-id]")
);
if (containerEl.getAttribute("data-l10n-id")) {
localizedEls.push(containerEl);
}
ok(localizedEls.length, "Got localized elements");
for (let el of localizedEls) {
const l10nId = el.getAttribute("data-l10n-id");
const l10nAttrs = el.getAttribute("data-l10n-attrs");
if (!l10nAttrs) {
await TestUtils.waitForCondition(
() => el.textContent !== "",
`Element with Fluent id '${l10nId}' should not be empty`
);
} else {
await TestUtils.waitForCondition(
() => el.message !== "",
`Message attribute of the element with Fluent id '${l10nId}'
should not be empty`
);
}
}
},
// Assert that the report action visibility on the addon card
// for the given about:addons windows and extension id.
async assertReportActionVisibility(gManagerWindow, extId, expectShown) {
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}`);
Assert.equal(
reportButton.hidden,
!expectShown,
`${extId} report action should be ${expectShown ? "shown" : "hidden"}`
);
},
// Assert that the report action is hidden on the addon card
// for the given about:addons windows and extension id.
assertReportActionHidden(gManagerWindow, extId) {
return this.assertReportActionVisibility(gManagerWindow, extId, false);
},
// Assert that the report action is shown on the addon card
// for the given about:addons windows and extension id.
assertReportActionShown(gManagerWindow, extId) {
return this.assertReportActionVisibility(gManagerWindow, extId, true);
},
// 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;
};
}