summaryrefslogtreecommitdiffstats
path: root/dom/midi/tests/browser_midi_permission_gated.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/midi/tests/browser_midi_permission_gated.js')
-rw-r--r--dom/midi/tests/browser_midi_permission_gated.js829
1 files changed, 829 insertions, 0 deletions
diff --git a/dom/midi/tests/browser_midi_permission_gated.js b/dom/midi/tests/browser_midi_permission_gated.js
new file mode 100644
index 0000000000..2367e8ec90
--- /dev/null
+++ b/dom/midi/tests/browser_midi_permission_gated.js
@@ -0,0 +1,829 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXAMPLE_COM_URL =
+ "https://example.com/document-builder.sjs?html=<h1>Test midi permission with synthetic site permission addon</h1>";
+const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html=
+ <h1>Test midi permission with synthetic site permission addon in iframes</h1>
+ <iframe id=sameOrigin src="${encodeURIComponent(
+ 'https://example.org/document-builder.sjs?html=SameOrigin"'
+ )}"></iframe>
+ <iframe id=crossOrigin src="${encodeURIComponent(
+ 'https://example.net/document-builder.sjs?html=CrossOrigin"'
+ )}"></iframe>`;
+
+const l10n = new Localization(
+ [
+ "browser/addonNotifications.ftl",
+ "toolkit/global/extensions.ftl",
+ "toolkit/global/extensionPermissions.ftl",
+ "branding/brand.ftl",
+ ],
+ true
+);
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["midi.prompt.testing", false]],
+ });
+
+ AddonTestUtils.initMochitest(this);
+ AddonTestUtils.hookAMTelemetryEvents();
+
+ // Once the addon is installed, a dialog is displayed as a confirmation.
+ // This could interfere with tests running after this one, so we set up a listener
+ // that will always accept post install dialogs so we don't have to deal with them in
+ // the test.
+ alwaysAcceptAddonPostInstallDialogs();
+
+ registerCleanupFunction(async () => {
+ // Remove the permission.
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: EXAMPLE_COM_URL,
+ });
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: PAGE_WITH_IFRAMES_URL,
+ });
+ await SpecialPowers.removePermission("midi", {
+ url: EXAMPLE_COM_URL,
+ });
+ await SpecialPowers.removePermission("midi", {
+ url: PAGE_WITH_IFRAMES_URL,
+ });
+ await SpecialPowers.removePermission("install", {
+ url: EXAMPLE_COM_URL,
+ });
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function testRequestMIDIAccess() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host;
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info("Request midi-sysex access");
+ let onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Deny site permission addon install in first popup");
+ let addonInstallPanel = await onAddonInstallBlockedNotification;
+ const [installPopupHeader, installPopupMessage] =
+ addonInstallPanel.querySelectorAll(
+ "description.popup-notification-description"
+ );
+ is(
+ installPopupHeader.textContent,
+ l10n.formatValueSync("site-permission-install-first-prompt-midi-header"),
+ "First popup has expected header text"
+ );
+ is(
+ installPopupMessage.textContent,
+ l10n.formatValueSync("site-permission-install-first-prompt-midi-message"),
+ "First popup has expected message"
+ );
+
+ let notification = addonInstallPanel.childNodes[0];
+ // secondaryButton is the "Don't allow" button
+ notification.secondaryButton.click();
+
+ let rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = `${e.name}: ${e.message}`;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError: WebMIDI requires a site permission add-on to activate"
+ );
+
+ assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]);
+
+ info("Deny site permission addon install in second popup");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+ let dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ let installDialog = await dialogPromise;
+ is(
+ installDialog.querySelector(".popup-notification-description").textContent,
+ l10n.formatValueSync(
+ "webext-site-perms-header-with-gated-perms-midi-sysex",
+ { hostname: testPageHost }
+ ),
+ "Install dialog has expected header text"
+ );
+ is(
+ installDialog.querySelector("popupnotificationcontent description")
+ .textContent,
+ l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
+ "Install dialog has expected description"
+ );
+
+ // secondaryButton is the "Cancel" button
+ installDialog.secondaryButton.click();
+
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = `${e.name}: ${e.message}`;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError: WebMIDI requires a site permission add-on to activate"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "cancelled",
+ ]);
+
+ info("Request midi-sysex access again");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Accept site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+ dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ installDialog = await dialogPromise;
+ installDialog.button.click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ let accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that midi-sysex is now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "but midi should have UNKNOWN permission"
+ );
+
+ info("Check that we don't prompt user again once they installed the addon");
+ const accessPromiseState = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.navigator
+ .requestMIDIAccess({ sysex: true })
+ .then(() => "resolved");
+ }
+ );
+ is(
+ accessPromiseState,
+ "resolved",
+ "requestMIDIAccess resolved without user prompt"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Request midi access without sysex");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiNoSysexAccessRequestPromise =
+ content.navigator.requestMIDIAccess();
+ });
+
+ info("Accept site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+
+ is(
+ notification
+ .querySelector("#addon-install-blocked-info")
+ .getAttribute("href"),
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "site-permission-addons",
+ "Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
+ );
+
+ dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ installDialog = await dialogPromise;
+
+ is(
+ installDialog.querySelector(".popup-notification-description").textContent,
+ l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", {
+ hostname: testPageHost,
+ }),
+ "Install dialog has expected header text"
+ );
+ is(
+ installDialog.querySelector("popupnotificationcontent description")
+ .textContent,
+ l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
+ "Install dialog has expected description"
+ );
+
+ installDialog.button.click();
+
+ info("Wait for the midi access request promise to resolve");
+ accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiNoSysexAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiNoSysexAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that both midi-sysex and midi are now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "and midi value should also have ALLOW permission"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Check that we don't prompt user again when they perm denied");
+ // remove permission to have a clean state
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: EXAMPLE_COM_URL,
+ });
+
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Perm-deny site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ // Click the "Report Suspicious Site" menuitem, which has the same effect as
+ // "Never Allow" and also submits a telemetry event (which we check below).
+ notification.menupopup.querySelectorAll("menuitem")[1].click();
+
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = e.name;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(rejectionMessage, "SecurityError", "requestMIDIAccess was rejected");
+
+ info("Request midi-sysex access again");
+ let denyIntervalStart = performance.now();
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ } catch (e) {
+ errorMessage = e.name;
+ }
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError",
+ "requestMIDIAccess was rejected without user prompt"
+ );
+ let denyIntervalElapsed = performance.now() - denyIntervalStart;
+ ok(
+ denyIntervalElapsed >= 3000,
+ `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${
+ denyIntervalElapsed / 1000
+ } seconds)`
+ );
+
+ // Invoking getAMTelemetryEvents resets the mocked event array, and we want
+ // to test two different things here, so we cache it.
+ let events = AddonTestUtils.getAMTelemetryEvents();
+ Assert.deepEqual(
+ events.filter(evt => evt.method == "reportSuspiciousSite")[0],
+ {
+ method: "reportSuspiciousSite",
+ object: "suspiciousSite",
+ value: "example.com",
+ extra: undefined,
+ }
+ );
+ assertSitePermissionInstallTelemetryEvents(
+ ["site_warning", "cancelled"],
+ events
+ );
+});
+
+add_task(async function testIframeRequestMIDIAccess() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ PAGE_WITH_IFRAMES_URL
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: PAGE_WITH_IFRAMES_URL }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info("Request midi-sysex access from the same-origin iframe");
+ const sameOriginIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.getElementById("sameOrigin").browsingContext;
+ }
+ );
+
+ let onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Accept site permission addon install");
+ const addonInstallPanel = await onAddonInstallBlockedNotification;
+ const notification = addonInstallPanel.childNodes[0];
+ const dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ let installDialog = await dialogPromise;
+ installDialog.button.click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ const accessGranted = await SpecialPowers.spawn(
+ sameOriginIframeBrowsingContext,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that midi-sysex is now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: PAGE_WITH_IFRAMES_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+
+ info(
+ "Check that we don't prompt user again once they installed the addon from the same-origin iframe"
+ );
+ const accessPromiseState = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.navigator
+ .requestMIDIAccess({ sysex: true })
+ .then(() => "resolved");
+ }
+ );
+ is(
+ accessPromiseState,
+ "resolved",
+ "requestMIDIAccess resolved without user prompt"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Check that request is rejected when done from a cross-origin iframe");
+ const crossOriginIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.getElementById("crossOrigin").browsingContext;
+ }
+ );
+
+ const onConsoleErrorMessage = new Promise(resolve => {
+ const errorListener = {
+ observe(error) {
+ if (error.message.includes("WebMIDI access request was denied")) {
+ resolve(error);
+ Services.console.unregisterListener(errorListener);
+ }
+ },
+ };
+ Services.console.registerListener(errorListener);
+ });
+
+ const rejectionMessage = await SpecialPowers.spawn(
+ crossOriginIframeBrowsingContext,
+ [],
+ async () => {
+ let errorName;
+ try {
+ await content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ } catch (e) {
+ errorName = e.name;
+ }
+ return errorName;
+ }
+ );
+
+ is(
+ rejectionMessage,
+ "SecurityError",
+ "requestMIDIAccess from the remote iframe was rejected"
+ );
+
+ const consoleErrorMessage = await onConsoleErrorMessage;
+ ok(
+ consoleErrorMessage.message.includes(
+ `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`,
+ "an error message is sent to the console"
+ )
+ );
+ assertSitePermissionInstallTelemetryEvents([]);
+});
+
+add_task(async function testRequestMIDIAccessLocalhost() {
+ const httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerPathHandler(`/test`, function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`
+ <!DOCTYPE html>
+ <meta charset=utf8>
+ <h1>Test requestMIDIAccess on lcoalhost</h1>`);
+ });
+ const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`;
+
+ registerCleanupFunction(async function cleanup() {
+ await new Promise(resolve => httpServer.stop(resolve));
+ });
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: localHostTestUrl }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info(
+ "Request midi-sysex access should not prompt for addon install on locahost, but for permission"
+ );
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+ await popupShown;
+ is(
+ PopupNotifications.panel.querySelector("popupnotification").id,
+ "midi-notification",
+ "midi notification was displayed"
+ );
+
+ info("Accept permission");
+ PopupNotifications.panel
+ .querySelector(".popup-notification-primary-button")
+ .click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ const accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that we prompt user again even if they accepted before");
+ popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ content.navigator.requestMIDIAccess({ sysex: true });
+ });
+ await popupShown;
+ is(
+ PopupNotifications.panel.querySelector("popupnotification").id,
+ "midi-notification",
+ "midi notification was displayed again"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([]);
+});
+
+add_task(async function testDisabledRequestMIDIAccessFile() {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append("blank.html");
+ const fileSchemeTestUri = Services.io.newFileURI(dir).spec;
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that requestMIDIAccess isn't set on navigator on file scheme");
+ const isRequestMIDIAccessDefined = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return "requestMIDIAccess" in content.wrappedJSObject.navigator;
+ }
+ );
+ is(
+ isRequestMIDIAccessDefined,
+ false,
+ "navigator.requestMIDIAccess is not defined on file scheme"
+ );
+});
+
+// Ignore any additional telemetry events collected in this file.
+// Unfortunately it doesn't work to have this in a cleanup function.
+// Keep this as the last task done.
+add_task(function teardown_telemetry_events() {
+ AddonTestUtils.getAMTelemetryEvents();
+});
+
+/**
+ * Check that the expected sitepermission install events are recorded.
+ *
+ * @param {Array<String>} expectedSteps: An array of the expected extra.step values recorded.
+ */
+function assertSitePermissionInstallTelemetryEvents(
+ expectedSteps,
+ events = null
+) {
+ let amInstallEvents = (events ?? AddonTestUtils.getAMTelemetryEvents())
+ .filter(evt => evt.method === "install" && evt.object === "sitepermission")
+ .map(evt => evt.extra.step);
+
+ Assert.deepEqual(amInstallEvents, expectedSteps);
+}
+
+async function waitForInstallDialog(id = "addon-webext-permissions") {
+ let panel = await waitForNotification(id);
+ return panel.childNodes[0];
+}
+
+/**
+ * Adds an event listener that will listen for post-install dialog event and automatically
+ * close the dialogs.
+ */
+function alwaysAcceptAddonPostInstallDialogs() {
+ // Once the addon is installed, a dialog is displayed as a confirmation.
+ // This could interfere with tests running after this one, so we set up a listener
+ // that will always accept post install dialogs so we don't have to deal with them in
+ // the test.
+ const abortController = new AbortController();
+
+ const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+ );
+ info("Start listening and accept addon post-install notifications");
+ PanelUI.notificationPanel.addEventListener(
+ "popupshown",
+ async function popupshown() {
+ let notification = AppMenuNotifications.activeNotification;
+ if (!notification || notification.id !== "addon-installed") {
+ return;
+ }
+
+ let popupnotificationID = PanelUI._getPopupId(notification);
+ if (popupnotificationID) {
+ info("Accept post-install dialog");
+ let popupnotification = document.getElementById(popupnotificationID);
+ popupnotification?.button.click();
+ }
+ },
+ {
+ signal: abortController.signal,
+ }
+ );
+
+ registerCleanupFunction(async () => {
+ // Clear the listener at the end of the test file, to prevent it to stay
+ // around when the same browser instance may be running other unrelated
+ // test files.
+ abortController.abort();
+ });
+}
+
+const PROGRESS_NOTIFICATION = "addon-progress";
+async function waitForNotification(notificationId) {
+ info(`Waiting for ${notificationId} notification`);
+
+ let topic = getObserverTopic(notificationId);
+
+ let observerPromise;
+ if (notificationId !== "addon-webext-permissions") {
+ observerPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ // Ignore the progress notification unless that is the notification we want
+ if (
+ notificationId != PROGRESS_NOTIFICATION &&
+ aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
+ ) {
+ return;
+ }
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, topic);
+ });
+ }
+
+ let panelEventPromise = new Promise(resolve => {
+ window.PopupNotifications.panel.addEventListener(
+ "PanelUpdated",
+ function eventListener(e) {
+ // Skip notifications that are not the one that we are supposed to be looking for
+ if (!e.detail.includes(notificationId)) {
+ return;
+ }
+ window.PopupNotifications.panel.removeEventListener(
+ "PanelUpdated",
+ eventListener
+ );
+ resolve();
+ }
+ );
+ });
+
+ await observerPromise;
+ await panelEventPromise;
+ await waitForTick();
+
+ info(`Saw a ${notificationId} notification`);
+ await SimpleTest.promiseFocus(window.PopupNotifications.window);
+ return window.PopupNotifications.panel;
+}
+
+// This function is similar to the one in
+// toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js,
+// please keep both in sync!
+function getObserverTopic(aNotificationId) {
+ let topic = aNotificationId;
+ if (topic == "xpinstall-disabled") {
+ topic = "addon-install-disabled";
+ } else if (topic == "addon-progress") {
+ topic = "addon-install-started";
+ } else if (topic == "addon-installed") {
+ topic = "webextension-install-notify";
+ }
+ return topic;
+}
+
+function waitForTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}