/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);
const { ExtensionPermissions } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionPermissions.sys.mjs"
);

const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
  return new Localization(
    [
      "browser/addonNotifications.ftl",
      "branding/brand.ftl",
      "toolkit/global/extensions.ftl",
    ],
    true
  );
});

const PROGRESS_NOTIFICATION = "addon-progress";

const CHROMEROOT = extractChromeRoot(gTestPath);

AddonTestUtils.initMochitest(this);

/* import-globals-from helpers_addons_install_dialogs.js */
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/toolkit/mozapps/extensions/test/xpinstall/helpers_addons_install_dialogs.js",
  this
);

let needsCleanupBlocklist = true;

const cleanupBlocklist = async () => {
  if (!needsCleanupBlocklist) {
    return;
  }
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash: { blocked: [], unblocked: [] },
        stash_time: 0,
      },
    ],
  });
  needsCleanupBlocklist = false;
};

async function waitForProgressNotification(
  aPanelOpen = false,
  aExpectedCount = 1,
  wantDisabled = true,
  expectedAnchorID = "unified-extensions-button",
  win = window
) {
  let notificationId = PROGRESS_NOTIFICATION;
  info("Waiting for " + notificationId + " notification");

  let topic = getObserverTopic(notificationId);

  let observerPromise = new Promise(resolve => {
    Services.obs.addObserver(function observer(aSubject, aTopic) {
      // 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;
  if (aPanelOpen) {
    panelEventPromise = Promise.resolve();
  } else {
    panelEventPromise = new Promise(resolve => {
      win.PopupNotifications.panel.addEventListener(
        "popupshowing",
        function () {
          resolve();
        },
        { once: true }
      );
    });
  }

  await observerPromise;
  await panelEventPromise;
  await TestUtils.waitForTick();

  info("Saw a notification");
  ok(win.PopupNotifications.isPanelOpen, "Panel should be open");
  is(
    win.PopupNotifications.panel.childNodes.length,
    aExpectedCount,
    "Should be the right number of notifications"
  );
  if (win.PopupNotifications.panel.childNodes.length) {
    let nodes = Array.from(win.PopupNotifications.panel.childNodes);
    let notification = nodes.find(
      n => n.id == notificationId + "-notification"
    );
    ok(notification, `Should have seen the right notification`);
    is(
      notification.button.hasAttribute("disabled"),
      wantDisabled,
      "The install button should be disabled?"
    );

    let n = win.PopupNotifications.getNotification(PROGRESS_NOTIFICATION);
    is(
      n?.anchorElement?.id || n?.anchorElement?.parentElement?.id,
      expectedAnchorID,
      "expected the right anchor ID"
    );
  }

  return win.PopupNotifications.panel;
}

function testInstallDialogIncognitoCheckbox(
  installDialog,
  {
    toggleIncognito = false,
    incognitoChecked = false,
    incognitoHidden = false,
  } = {}
) {
  // If the incognito toggle is expected to be in the first install dialog
  // verify that it is found and visible and toggle it.
  const privateBrowsingCheckbox = installDialog.querySelector(
    ".webext-perm-privatebrowsing moz-checkbox"
  );
  is(
    !privateBrowsingCheckbox,
    incognitoHidden,
    incognitoHidden
      ? "Expect private browsing checkbox to NOT be found in the first dialog"
      : "Expect private browsing checkbox to be found in the first install dialog"
  );

  if (!incognitoHidden) {
    if (privateBrowsingCheckbox) {
      ok(
        BrowserTestUtils.isVisible(privateBrowsingCheckbox),
        "private browsing checkbox should be visible"
      );
    }
    // SUMO link should always be visible if the incognito checkbox is expected to be
    // shown too (even when there are no other permissions being granted as part of the
    // same install dialog).
    let permsLearnMore = installDialog.querySelector(
      ".popup-notification-learnmore-link"
    );
    is(
      permsLearnMore.href,
      Services.urlFormatter.formatURLPref("app.support.baseURL") +
        "extension-permissions",
      "Learn more link has desired URL"
    );
    ok(
      BrowserTestUtils.isVisible(permsLearnMore),
      "SUMO link expected to be visible"
    );
  } else {
    if (incognitoChecked) {
      throw new Error(
        "incognitoChecked can't be set to true when incognitoHidden is set to true"
      );
    }
    if (toggleIncognito) {
      throw new Error(
        "toggleIncognito can't be set to true when incognitoHidden is set to true"
      );
    }
    return;
  }

  is(
    privateBrowsingCheckbox.checked,
    incognitoChecked,
    incognitoChecked
      ? "Expect private browsing checkbox to be checked"
      : "Expect private browsing checkbox to NOT be checked"
  );

  if (toggleIncognito === true) {
    privateBrowsingCheckbox.click();
  }
}

function acceptAppMenuNotificationWhenShown(
  id,
  extensionId,
  { dismiss = false, global = window } = {}
) {
  const { AppMenuNotifications, PanelUI, document } = global;
  return new Promise(resolve => {
    let permissionChangePromise = null;
    function appMenuPopupHidden() {
      PanelUI.panel.removeEventListener("popuphidden", appMenuPopupHidden);
      ok(
        !PanelUI.menuButton.hasAttribute("badge-status"),
        "badge is not set after addon-installed"
      );
      resolve(permissionChangePromise);
    }
    function appMenuPopupShown() {
      PanelUI.panel.removeEventListener("popupshown", appMenuPopupShown);
      PanelUI.menuButton.click();
    }
    function popupshown() {
      let notification = AppMenuNotifications.activeNotification;
      if (!notification) {
        return;
      }

      is(notification.id, id, `${id} notification shown`);
      ok(PanelUI.isNotificationPanelOpen, "notification panel open");

      PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);

      if (dismiss) {
        // Dismiss the panel by clicking on the appMenu button.
        PanelUI.panel.addEventListener("popupshown", appMenuPopupShown);
        PanelUI.panel.addEventListener("popuphidden", appMenuPopupHidden);
        PanelUI.menuButton.click();
        return;
      }

      // Dismiss the panel by clicking the primary button.
      let popupnotificationID = PanelUI._getPopupId(notification);
      let popupnotification = document.getElementById(popupnotificationID);

      popupnotification.button.click();
      resolve(permissionChangePromise);
    }
    PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
  });
}

function waitForNotificationClose(win = window) {
  if (!win.PopupNotifications.isPanelOpen) {
    return Promise.resolve();
  }
  return new Promise(resolve => {
    info("Waiting for notification to close");
    win.PopupNotifications.panel.addEventListener(
      "popuphidden",
      function () {
        resolve();
      },
      { once: true }
    );
  });
}

function removeTabAndWaitForNotificationClose() {
  let closePromise = waitForNotificationClose();
  BrowserTestUtils.removeTab(gBrowser.selectedTab);
  return closePromise;
}

function acceptInstallDialog(installDialog) {
  installDialog.button.click();
}

async function waitForSingleNotification() {
  while (PopupNotifications.panel.childNodes.length != 1) {
    await new Promise(resolve => executeSoon(resolve));

    info("Waiting for single notification");
    // Notification should never close while we wait
    ok(PopupNotifications.isPanelOpen, "Notification should still be open");
  }
}

async function setupRedirect(aSettings) {
  var baseURL = `${SECURE_TESTROOT}/redirect.sjs`;
  const params = new URLSearchParams(aSettings);
  params.set("mode", "setup");
  await fetch(`${baseURL}?${params}`);
}

async function installAddonWithPrivateBrowsingAccess(xpiUrl, addonId) {
  // Note: this used to be effective, but changed in bug 1974419. Since then,
  // the private browsing permission is only read from the database if an
  // add-on was already installed at the time of prompting.
  await ExtensionPermissions.add(addonId, {
    permissions: ["internal:privateBrowsingAllowed"],
    origins: [],
  });

  let progressPromise = waitForProgressNotification();
  let dialogPromise = waitForInstallDialog();

  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
    wantLoad: "about:blank",
  });
  gURLBar.value = xpiUrl;
  gURLBar.focus();
  EventUtils.synthesizeKey("KEY_Enter");

  await progressPromise;
  let installDialog = await dialogPromise;

  testInstallDialogIncognitoCheckbox(installDialog, {
    incognitoHidden: false,
    // Note: before the change in bug 1974419, this expectation was true.
    incognitoChecked: false,
    toggleIncognito: true,
  });
  let notificationPromise = acceptAppMenuNotificationWhenShown(
    "addon-installed",
    addonId
  );
  let readyPromise = AddonTestUtils.promiseWebExtensionStartup(addonId);
  installDialog.button.click();
  await notificationPromise;
  let installs = await AddonManager.getAllInstalls();
  is(installs.length, 0, "Should be no pending installs");

  await readyPromise;

  let policy = WebExtensionPolicy.getByID(addonId);
  ok(policy.privateBrowsingAllowed, "private browsing permission granted");

  await removeTabAndWaitForNotificationClose();
}

add_setup(async function () {
  requestLongerTimeout(4);

  await SpecialPowers.pushPrefEnv({
    set: [
      ["extensions.logging.enabled", true],
      ["security.dialog_enable_delay", 0],
    ],
  });

  const XPInstallObserver = {
    observe(aSubject, aTopic) {
      let installInfo = aSubject.wrappedJSObject;
      info(`Observed ${aTopic} for ${installInfo.installs.length} installs`);
      installInfo.installs.forEach(aInstall =>
        info(
          `Install of ${aInstall.sourceURI.spec} was in state ${aInstall.state}`
        )
      );
    },
  };

  Services.obs.addObserver(XPInstallObserver, "addon-install-started");
  Services.obs.addObserver(XPInstallObserver, "addon-install-blocked");
  Services.obs.addObserver(XPInstallObserver, "addon-install-failed");

  registerCleanupFunction(async function () {
    let aInstalls = await AddonManager.getAllInstalls();
    aInstalls.forEach(function (aInstall) {
      aInstall.cancel();
    });

    Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
    Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
    Services.obs.removeObserver(XPInstallObserver, "addon-install-failed");
  });
});

describe("Add-on installation doorhangers", function () {
  async function beforeAndAfterEach() {
    ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no active installs");
  }
  beforeEach(beforeAndAfterEach);
  afterEach(beforeAndAfterEach);

  it("should show an 'install disabled' notification when xpinstall.enabled=false that lets the user enable installation", async function test_disabledInstall() {
    await SpecialPowers.pushPrefEnv({
      set: [["xpinstall.enabled", false]],
    });
    let notificationPromise = waitForNotification("xpinstall-disabled");
    let xpiURL = TESTROOT + "amosigned.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;

    let notification = panel.childNodes[0];
    is(
      notification.button.label,
      "Enable",
      "Should have seen the right button"
    );
    is(
      notification.getAttribute("label"),
      "Software installation is currently disabled. Click Enable and try again.",
      "notification label is correct"
    );

    let closePromise = waitForNotificationClose();
    // Click on Enable
    EventUtils.synthesizeMouseAtCenter(notification.button, {});
    await closePromise;

    try {
      ok(
        Services.prefs.getBoolPref("xpinstall.enabled"),
        "Installation should be enabled"
      );
    } catch (e) {
      ok(false, "xpinstall.enabled should be set");
    }

    BrowserTestUtils.removeTab(gBrowser.selectedTab);
    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Shouldn't be any pending installs");
    await SpecialPowers.popPrefEnv();
  });

  it("should show addon-install-blocked for 3rd party installs and proceed to installation after being allowed by the user", async function test_blockedInstall() {
    await SpecialPowers.pushPrefEnv({
      set: [["extensions.postDownloadThirdPartyPrompt", false]],
    });

    let notificationPromise = waitForNotification("addon-install-blocked");
    let xpiURL = TESTROOT + "amosigned.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;

    let notification = panel.childNodes[0];
    is(
      notification.button.label,
      "Continue to Installation",
      "Should have seen the right button"
    );
    is(
      notification
        .querySelector("#addon-install-blocked-info")
        .getAttribute("href"),
      Services.urlFormatter.formatURLPref("app.support.baseURL") +
        "unlisted-extensions-risks",
      "Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
    );
    let message = panel.ownerDocument.getElementById(
      "addon-install-blocked-message"
    );
    is(
      message.textContent,
      "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
      "Should have seen the right message"
    );

    let dialogPromise = waitForInstallDialog();
    // Click on Allow
    EventUtils.synthesizeMouse(notification.button, 20, 10, {});

    // Notification should have changed to progress notification
    ok(PopupNotifications.isPanelOpen, "Notification should still be open");
    notification = panel.childNodes[0];
    is(
      notification.id,
      "addon-progress-notification",
      "Should have seen the progress notification"
    );

    let installDialog = await dialogPromise;

    notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org"
    );

    installDialog.button.click();
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );
    await addon.uninstall();

    await BrowserTestUtils.removeTab(gBrowser.selectedTab);
    await SpecialPowers.popPrefEnv();
  });

  it("should show the 3rd party install panel when installing add-ons from websites", async function test_blockedPostDownload() {
    await SpecialPowers.pushPrefEnv({
      set: [["extensions.postDownloadThirdPartyPrompt", true]],
    });

    let notificationPromise = waitForNotification("addon-install-blocked");
    let xpiURL = TESTROOT + "amosigned.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;

    let notification = panel.childNodes[0];
    is(
      notification.button.label,
      "Continue to Installation",
      "Should have seen the right button"
    );
    let message = panel.ownerDocument.getElementById(
      "addon-install-blocked-message"
    );
    is(
      message.textContent,
      "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
      "Should have seen the right message"
    );

    let dialogPromise = waitForInstallDialog();
    // Click on Allow
    EventUtils.synthesizeMouse(notification.button, 20, 10, {});

    let installDialog = await dialogPromise;

    notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org"
    );

    installDialog.button.click();
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );
    await addon.uninstall();

    await BrowserTestUtils.removeTab(gBrowser.selectedTab);
    await SpecialPowers.popPrefEnv();
  });

  it("should not show the 3rd party install panel when installing recommended add-ons", async function test_recommendedPostDownload() {
    await SpecialPowers.pushPrefEnv({
      set: [
        ["extensions.postDownloadThirdPartyPrompt", true],
        // recommended.xpi is signed with AMO staging signature.
        ["xpinstall.signatures.dev-root", true],
        // Disable the blocklist because recommended.xpi is signed
        // only with the staging signature and it may end up hitting
        // a false positive on the Blocklist bloomfilter computed
        // from all add-ons known by AMO as signed with the
        // production key (e.g. see Bug 1996059).
        ["extensions.blocklist.enabled", false],
      ],
    });

    let xpiURL = TESTROOT + "recommended.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );

    let installDialog = await waitForInstallDialog();

    let notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "recommended-line@test.mozilla.org"
    );

    installDialog.button.click();
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "recommended-line@test.mozilla.org"
    );
    await addon.uninstall();

    await BrowserTestUtils.removeTab(gBrowser.selectedTab);
    await SpecialPowers.popPrefEnv();
  });

  it("should not show the 3rd party install panel when installing privileged add-ons", async function test_priviledgedNo3rdPartyPrompt() {
    await SpecialPowers.pushPrefEnv({
      set: [["extensions.postDownloadThirdPartyPrompt", true]],
    });
    AddonManager.checkUpdateSecurity = false;
    registerCleanupFunction(() => {
      AddonManager.checkUpdateSecurity = true;
    });

    let xpiURL = TESTROOT + "privileged.xpi";

    let installDialogPromise = waitForInstallDialog();

    try {
      // Prevent install to fail due to privileged.xpi version using
      // an addon version that hits a manifest warning (see PRIV_ADDON_VERSION).
      // TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a
      // version format that does not hit a manifest warning.
      ExtensionTestUtils.failOnSchemaWarnings(false);
      let tab = await BrowserTestUtils.openNewForegroundTab(
        gBrowser,
        TESTROOT + "navigate.html?" + xpiURL
      );

      let notificationPromise = acceptAppMenuNotificationWhenShown(
        "addon-installed",
        "test@tests.mozilla.org"
      );

      const installDialog = await installDialogPromise;

      testInstallDialogIncognitoCheckbox(installDialog, {
        incognitoHidden: true,
      });

      installDialog.button.click();
      await notificationPromise;

      let installs = await AddonManager.getAllInstalls();
      is(installs.length, 0, "Should be no pending installs");

      let addon = await AddonManager.getAddonByID("test@tests.mozilla.org");
      await addon.uninstall();

      await BrowserTestUtils.removeTab(tab);
    } finally {
      ExtensionTestUtils.failOnSchemaWarnings(true);
    }

    await SpecialPowers.popPrefEnv();
    AddonManager.checkUpdateSecurity = true;
  });

  it("should permanently block installations from a site when the user chooses 'Never Allow'", async function test_permaBlockInstall() {
    let notificationPromise = waitForNotification("addon-install-blocked");
    let xpiURL = TESTROOT + "amosigned.xpi";
    let target = TESTROOT + "navigate.html?" + xpiURL;

    BrowserTestUtils.openNewForegroundTab(gBrowser, target);
    let notification = (await notificationPromise).firstElementChild;
    let neverAllowBtn = notification.menupopup.firstElementChild;

    neverAllowBtn.click();

    await TestUtils.waitForCondition(
      () => !PopupNotifications.isPanelOpen,
      "Waiting for notification to close"
    );

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let installPerm = PermissionTestUtils.testPermission(
      gBrowser.currentURI,
      "install"
    );
    is(
      installPerm,
      Ci.nsIPermissionManager.DENY_ACTION,
      "Addon installation should be blocked for site"
    );

    await BrowserTestUtils.removeTab(gBrowser.selectedTab);

    PermissionTestUtils.remove(target, "install");
  });

  it("should not show any installation prompt for a site that is permanently blocked", async function test_permaBlockedInstallNoPrompt() {
    let xpiURL = TESTROOT + "amosigned.xpi";
    let target = TESTROOT + "navigate.html?" + xpiURL;

    PermissionTestUtils.add(target, "install", Services.perms.DENY_ACTION);
    await BrowserTestUtils.openNewForegroundTab(gBrowser, target);

    let panelOpened;
    try {
      panelOpened = await TestUtils.waitForCondition(
        () => PopupNotifications.isPanelOpen,
        100,
        10
      );
    } catch (ex) {
      panelOpened = false;
    }
    is(panelOpened, false, "Addon prompt should not open");

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    await BrowserTestUtils.removeTab(gBrowser.selectedTab);

    PermissionTestUtils.remove(target, "install");
  });

  it("should proceed directly to the install dialog for a site that already has permission to install add-ons", async function test_allowedInstall() {
    let originalTab = gBrowser.selectedTab;
    let tab;
    gBrowser.selectedTab = originalTab;
    PermissionTestUtils.add(
      "https://example.com/",
      "install",
      Services.perms.ALLOW_ACTION
    );

    let progressPromise = waitForProgressNotification();
    let dialogPromise = waitForInstallDialog();
    let xpiURL = TESTROOT + "amosigned.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      SECURE_TESTROOT + "navigate.html?" + xpiURL
    ).then(newTab => (tab = newTab));
    await progressPromise;
    let installDialog = await dialogPromise;
    await TestUtils.waitForCondition(() => !!tab, "tab should be present");

    is(
      gBrowser.selectedTab,
      tab,
      "tab selected in response to the addon-install-confirmation notification"
    );

    let notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org",
      { dismiss: true }
    );
    let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
      "amosigned-xpi@tests.mozilla.org"
    );
    acceptInstallDialog(installDialog);

    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );

    await readyPromise;
    let readyPromise2 = AddonTestUtils.promiseWebExtensionStartup(
      "amosigned-xpi@tests.mozilla.org"
    );

    // Test that the addon does not have permission. Reload it to ensure it would
    // have been set if possible.
    await addon.reload();
    await readyPromise2;
    let policy = WebExtensionPolicy.getByID(addon.id);
    ok(
      !policy.privateBrowsingAllowed,
      "private browsing permission was not granted"
    );

    await addon.uninstall();

    PermissionTestUtils.remove("https://example.com/", "install");

    await removeTabAndWaitForNotificationClose();
  });

  it("should show a connection failure notification when the add-on download fails", async function test_failedDownload() {
    PermissionTestUtils.add(
      "http://example.com/",
      "install",
      Services.perms.ALLOW_ACTION
    );

    let gotFirstRequest = false;
    const server = AddonTestUtils.createHttpServer({
      hosts: ["fail.example.com"],
    });
    server.registerPathHandler(
      "/failed-xpi-download.xpi",
      (request, response) => {
        response.setStatusLine(request.httpVersion, 200, "OK");
        response.setHeader("Content-Type", "application/x-xpinstall");
        if (!gotFirstRequest) {
          // The first request is expected to be the one intercepted by amContentHandler.sys.mjs,
          // which will just look to the content-type to detect it as an add-on installation,
          // then the request is cancelled and a new one retriggered as part of the actual
          // add-on install flow.
          gotFirstRequest = true;
        } else {
          // This second request is the actual download. We respond with an
          // error to simulate a network failure.
          response.setStatusLine(request.httpVersion, 502, "Bad Gateway");
        }
      }
    );

    let progressPromise = waitForProgressNotification();
    let failPromise = waitForNotification("addon-install-failed");
    let xpiURL = "http://fail.example.com/failed-xpi-download.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    await progressPromise;
    let panel = await failPromise;

    let notification = panel.childNodes[0];
    is(
      notification.getAttribute("label"),
      "The add-on could not be downloaded because of a connection failure.",
      "Should have seen the right message"
    );

    PermissionTestUtils.remove("http://example.com/", "install");
    await removeTabAndWaitForNotificationClose();
  });

  it("should show a 'corrupt file' notification when the downloaded add-on is corrupt", async function test_corruptFile() {
    PermissionTestUtils.add(
      "http://example.com/",
      "install",
      Services.perms.ALLOW_ACTION
    );

    let progressPromise = waitForProgressNotification();
    let failPromise = waitForNotification("addon-install-failed");
    let xpiURL = TESTROOT + "corrupt.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    await progressPromise;
    let panel = await failPromise;

    let notification = panel.childNodes[0];
    is(
      notification.getAttribute("label"),
      "The add-on downloaded from this site could not be installed " +
        "because it appears to be corrupt.",
      "Should have seen the right message"
    );

    PermissionTestUtils.remove("http://example.com/", "install");
    await removeTabAndWaitForNotificationClose();
  });

  it("should show an incompatibility notification when the add-on is not compatible with the Firefox version", async function test_incompatible() {
    PermissionTestUtils.add(
      "http://example.com/",
      "install",
      Services.perms.ALLOW_ACTION
    );

    let progressPromise = waitForProgressNotification();
    let failPromise = waitForNotification("addon-install-failed");
    let xpiURL = TESTROOT + "incompatible.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    await progressPromise;
    let panel = await failPromise;

    let notification = panel.childNodes[0];
    let brandBundle = Services.strings.createBundle(
      "chrome://branding/locale/brand.properties"
    );
    let brandShortName = brandBundle.GetStringFromName("brandShortName");
    let message = `XPI Test could not be installed because it is not compatible with ${brandShortName} ${Services.appinfo.version}.`;
    is(
      notification.getAttribute("label"),
      message,
      "Should have seen the right message"
    );

    PermissionTestUtils.remove("http://example.com/", "install");
    await removeTabAndWaitForNotificationClose();
  });

  it.skipIf(
    "should show a warning in the installation dialog when the add-on is not signed",
    () => AppConstants.MOZ_REQUIRE_SIGNING,
    async function test_unsigned() {
      let progressPromise = waitForProgressNotification();
      let notificationPromise = waitForNotification("addon-install-blocked");
      let xpiURL = TESTROOT + "unsigned.xpi";
      BrowserTestUtils.openNewForegroundTab(
        gBrowser,
        TESTROOT + "navigate.html?" + xpiURL
      );
      await progressPromise;
      let panel = await notificationPromise;

      let notification = panel.childNodes[0];
      let message = panel.ownerDocument.getElementById(
        "addon-install-blocked-message"
      );
      is(
        message.textContent,
        "You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
        "Should have seen the right message"
      );

      let dialogPromise = waitForInstallDialog();
      // Click on Allow
      EventUtils.synthesizeMouse(notification.button, 20, 10, {});

      let installDialog = await dialogPromise;
      message = lazy.l10n.formatValueSync(`webext-perms-list-intro-unsigned`);
      is(
        installDialog.introEl.textContent,
        message,
        "Should have seen the right message"
      );
      is(
        installDialog.getAttribute("icon"),
        "chrome://global/skin/icons/warning.svg",
        "Expect panel icon to be set to the warning icon"
      );

      await removeTabAndWaitForNotificationClose();
    }
  );

  it("should show the appropriate blocklist notification for hard-blocked and soft-blocked add-ons", async function test_blocklisted() {
    let addonName = "XPI Test";
    let id = "amosigned-xpi@tests.mozilla.org";
    let version = "2.2";

    const {
      BlocklistPrivate: { ExtensionBlocklistMLBF },
    } = ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs");

    await SpecialPowers.pushPrefEnv({
      set: [
        [
          "extensions.blocklist.addonItemURL",
          "https://example.com/blocked-addon/%addonID%/%addonVersion%/",
        ],
      ],
    });

    const blocklistURL = ExtensionBlocklistMLBF.createBlocklistURL(id, version);

    info("Verify addon-install-failed on hard-blocked addon");
    await testBlocklistedAddon({
      stash: { blocked: [`${id}:${version}`], unblocked: [] },
      expected: {
        fluentId: "addon-install-error-hard-blocked",
        blocklistURL,
      },
    });

    info("Verify addon-install-failed on soft-blocked blocked addon");
    await SpecialPowers.pushPrefEnv({
      set: [["extensions.blocklist.softblock.enabled", true]],
    });
    await testBlocklistedAddon({
      stash: { softblocked: [`${id}:${version}`], blocked: [], unblocked: [] },
      expected: {
        fluentId: "addon-install-error-soft-blocked2",
        blocklistURL,
      },
    });
    // Clear extensions.blocklist.softblock.enabled pref.
    await SpecialPowers.popPrefEnv();
    // Clear extensions.blocklist.addonItemURL pref.
    await SpecialPowers.popPrefEnv();

    async function testBlocklistedAddon({ stash, expected }) {
      await AddonTestUtils.loadBlocklistRawData({
        extensionsMLBF: [{ stash, stash_time: 0 }],
      });
      needsCleanupBlocklist = true;
      registerCleanupFunction(cleanupBlocklist);

      PermissionTestUtils.add(
        "http://example.com/",
        "install",
        Services.perms.ALLOW_ACTION
      );

      let progressPromise = waitForProgressNotification();
      let failPromise = waitForNotification("addon-install-failed-blocklist");
      let xpiURL = TESTROOT + "amosigned.xpi";
      BrowserTestUtils.openNewForegroundTab(
        gBrowser,
        TESTROOT + "navigate.html?" + xpiURL
      );
      await progressPromise;
      info("Wait for addon-install-failed notification");

      let panel = await failPromise;

      let notification = panel.childNodes[0];
      let message = lazy.l10n.formatValueSync(expected.fluentId, { addonName });
      is(
        notification.getAttribute("label"),
        message,
        "Should have seen the right message"
      );

      await TestUtils.waitForCondition(
        () => panel.state === "open",
        "Wait for the panel to reach the open state"
      );
      let blocklistURLEl = panel.querySelector(
        "#addon-install-failed-blocklist-info"
      );
      ok(
        BrowserTestUtils.isVisible(blocklistURLEl),
        "Expect blocklist info link to be visible"
      );
      is(
        blocklistURLEl.getAttribute("href"),
        expected.blocklistURL,
        "Blocklist info link href should be set to the expected url"
      );
      is(
        blocklistURLEl.textContent,
        await panel.ownerDocument.l10n.formatValue(
          "popup-notification-xpinstall-prompt-block-url"
        ),
        "Blocklist info link should have the expected localized string"
      );

      // Clicking on the the blocklistURL link is expected to dismiss the
      // popup.
      let closePromise = waitForNotificationClose();

      let newTabPromise = BrowserTestUtils.waitForNewTab(
        gBrowser,
        expected.blocklistURL,
        true
      );
      info(
        `Click on the blocklist "See details" link ${expected.blocklistURL}`
      );
      blocklistURLEl.click();
      const newTab = await newTabPromise;

      is(
        newTab,
        gBrowser.selectedTab,
        "Blocklist info tab is currrently selected"
      );
      BrowserTestUtils.removeTab(newTab);

      await cleanupBlocklist();
      PermissionTestUtils.remove("http://example.com/", "install");

      BrowserTestUtils.removeTab(gBrowser.selectedTab);
      await closePromise;
    }
  });

  it("should show a 'corrupt file' failure notification when installing a local corrupt file", async function test_localFile() {
    let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
      Ci.nsIChromeRegistry
    );
    let path;
    try {
      path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec;
    } catch (ex) {
      path = CHROMEROOT + "corrupt.xpi";
    }

    let failPromise = new Promise(resolve => {
      Services.obs.addObserver(function observer() {
        Services.obs.removeObserver(observer, "addon-install-failed");
        resolve();
      }, "addon-install-failed");
    });
    gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
      wantLoad: "about:blank",
    });
    BrowserTestUtils.startLoadingURIString(gBrowser, path);
    await failPromise;

    // Wait for the browser code to add the failure notification
    await waitForSingleNotification();

    let notification = PopupNotifications.panel.childNodes[0];
    is(
      notification.id,
      "addon-install-failed-notification",
      "Should have seen the install fail"
    );
    is(
      notification.getAttribute("label"),
      "This add-on could not be installed because it appears to be corrupt.",
      "Should have seen the right message"
    );

    await removeTabAndWaitForNotificationClose();
  });

  it("should handle add-on installation initiated from the address bar", async function test_urlBar() {
    let progressPromise = waitForProgressNotification();
    let dialogPromise = waitForInstallDialog();

    gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
      wantLoad: "about:blank",
    });
    gURLBar.value = TESTROOT + "amosigned.xpi";
    gURLBar.focus();
    EventUtils.synthesizeKey("KEY_Enter");

    await progressPromise;
    let installDialog = await dialogPromise;

    testInstallDialogIncognitoCheckbox(installDialog, {
      incognitoHidden: false,
      incognitoChecked: false,
      toggleIncognito: true,
    });

    let notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org"
    );
    installDialog.button.click();
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );
    // The panel is reloading the addon due to the permission change, we need some way
    // to wait for the reload to finish. addon.startupPromise doesn't do it for
    // us, so we'll just restart again.
    await addon.reload();

    // This addon should have private browsing permission.
    let policy = WebExtensionPolicy.getByID(addon.id);
    ok(policy.privateBrowsingAllowed, "private browsing permission granted");

    await addon.uninstall();

    await removeTabAndWaitForNotificationClose();
  });

  it("should show addon-install-failed when failing to install on a navigation that is triggered by the system principal", async function test_wrongHost() {
    gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);

    let loadedPromise = BrowserTestUtils.browserLoaded(
      gBrowser.selectedBrowser,
      false,
      TESTROOT2
    );
    BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT2);
    await loadedPromise;

    let progressPromise = waitForProgressNotification();
    let notificationPromise = waitForNotification("addon-install-failed");
    BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "corrupt.xpi");
    await progressPromise;
    let panel = await notificationPromise;

    let notification = panel.childNodes[0];
    is(
      notification.getAttribute("label"),
      "The add-on downloaded from this site could not be installed " +
        "because it appears to be corrupt.",
      "Should have seen the right message"
    );

    await removeTabAndWaitForNotificationClose();
  });

  it("should re-notify the user about blocked install if they dismiss the panel then trigger it again", async function test_renotifyBlocked() {
    let notificationPromise = waitForNotification("addon-install-blocked");
    let xpiURL = TESTROOT + "amosigned.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;

    let closePromise = waitForNotificationClose();
    // hide the panel (this simulates the user dismissing it)
    panel.hidePopup();
    await closePromise;

    info("Timeouts after this probably mean bug 589954 regressed");

    await new Promise(resolve => executeSoon(resolve));

    notificationPromise = waitForNotification("addon-install-blocked");
    BrowserTestUtils.startLoadingURIString(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 2, "Should be two pending installs");

    await removeTabAndWaitForNotificationClose(gBrowser.selectedTab);

    installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should have cancelled the installs");
  });

  it("should cancel the installation when the user clicks cancel on the download progress notification", async function test_cancel() {
    PermissionTestUtils.add(
      "http://example.com/",
      "install",
      Services.perms.ALLOW_ACTION
    );

    let gotFirstRequest = false;
    let hangingResponse;
    let hangingResponseCleanup = () => {
      hangingResponse?.finish();
      hangingResponse = null;
    };
    registerCleanupFunction(hangingResponseCleanup);
    const server = AddonTestUtils.createHttpServer({
      hosts: ["slow.example.com"],
    });
    server.registerPathHandler(
      "/slow-xpi-download.xpi",
      (request, response) => {
        response.setStatusLine(request.httpVersion, 200, "OK");
        response.setHeader("Content-Type", "application/x-xpinstall");
        if (!gotFirstRequest) {
          // The first request is expected to be the one intercepted by amContentHandler.sys.mjs,
          // which will just look to the content-type to detect it as an add-on installation,
          // then the request is cancelled and a new one retriggered as part of the actual
          // add-on install flow.
          gotFirstRequest = true;
        } else {
          // This second request is meant to be the one that is part of the actual add-on
          // install flow and we leave it pending so that the test can then reliably
          // cancel it from the download progress notification.
          hangingResponse = response;
          response.processAsync();
        }
      }
    );

    let notificationPromise = waitForNotification(PROGRESS_NOTIFICATION);
    let xpiURL = "http://slow.example.com/slow-xpi-download.xpi";
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;
    let notification = panel.childNodes[0];

    ok(PopupNotifications.isPanelOpen, "Notification should still be open");
    is(
      PopupNotifications.panel.childNodes.length,
      1,
      "Should be only one notification"
    );
    is(
      notification.id,
      "addon-progress-notification",
      "Should have seen the progress notification"
    );

    // Cancel the download
    let install = notification.notification.options.installs[0];
    let cancelledPromise = new Promise(resolve => {
      install.addListener({
        onDownloadCancelled() {
          install.removeListener(this);
          resolve();
        },
      });
    });
    EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
    await cancelledPromise;

    await TestUtils.waitForTick();

    ok(!PopupNotifications.isPanelOpen, "Notification should be closed");

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending install");

    PermissionTestUtils.remove("http://example.com/", "install");
    BrowserTestUtils.removeTab(gBrowser.selectedTab);
    hangingResponseCleanup();
  });

  it("should fail the installation for an https redirect to an insecure http URL without a fallback hash", async function test_failedSecurity() {
    await SpecialPowers.pushPrefEnv({
      set: [
        [PREF_INSTALL_REQUIREBUILTINCERTS, false],
        ["extensions.postDownloadThirdPartyPrompt", false],
      ],
    });

    // Configures redirect.sjs to redirect to a http url through the Location
    // header, this is expected to be triggering a download failure with an
    // ERROR_NETWORK_FAILURE error due to the https url redirecting to an xpi
    // to be downloaded from an insecure http url
    // (and no hash provided to allow installing it from an insecure url).
    setupRedirect({
      Location: TESTROOT + "amosigned.xpi",
    });

    let notificationPromise = waitForNotification("addon-install-failed");

    let tab = await BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      SECURE_TESTROOT
    );
    let install = await AddonManager.getInstallForURL(
      SECURE_TESTROOT + "redirect.sjs?mode=redirect"
    );
    AddonManager.installAddonFromWebpage(
      "application/x-xpinstall",
      tab.linkedBrowser,
      Services.scriptSecurityManager.getSystemPrincipal(),
      install
    );
    await notificationPromise;

    ok(PopupNotifications.isPanelOpen, "Notification should still be open");
    is(
      PopupNotifications.panel.childNodes.length,
      1,
      "Should be only one notification"
    );
    let notification = PopupNotifications.panel.childNodes[0];
    is(
      notification.id,
      "addon-install-failed-notification",
      "Should have seen the install fail"
    );

    await removeTabAndWaitForNotificationClose();
    await SpecialPowers.popPrefEnv();
  });

  // Verifies that incognito checkbox is checked if add-on was already
  // installed before, with private access. Regression test for bug 1581852.
  it("should revoke incognito permission when users uncheck the checkbox on re-installing add-on with granted incognito permission", async function test_incognito_checkbox() {
    // Grant permission up front.
    await installAddonWithPrivateBrowsingAccess(
      TESTROOT + "amosigned.xpi",
      "amosigned-xpi@tests.mozilla.org"
    );
    // ^ the above add-on will be overwritten by the install below, and removed
    // at the end of this task.

    let progressPromise = waitForProgressNotification();
    let dialogPromise = waitForInstallDialog();

    gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
      wantLoad: "about:blank",
    });
    gURLBar.value = TESTROOT + "amosigned.xpi";
    gURLBar.focus();
    EventUtils.synthesizeKey("KEY_Enter");

    await progressPromise;
    let installDialog = await dialogPromise;

    testInstallDialogIncognitoCheckbox(installDialog, {
      incognitoHidden: false,
      incognitoChecked: true,
      toggleIncognito: true,
    });

    let notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org"
    );
    let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
      "amosigned-xpi@tests.mozilla.org"
    );
    installDialog.button.click();
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );

    await readyPromise;

    // This addon should no longer have private browsing permission.
    let policy = WebExtensionPolicy.getByID(addon.id);
    ok(!policy.privateBrowsingAllowed, "private browsing permission removed");

    await addon.uninstall();

    await removeTabAndWaitForNotificationClose();
  });

  it("should correctly handle the incognito checkbox toggle during re-install in a new window", async function test_incognito_checkbox_new_window() {
    // Grant permission up front.
    await installAddonWithPrivateBrowsingAccess(
      TESTROOT + "amosigned.xpi",
      "amosigned-xpi@tests.mozilla.org"
    );
    // ^ the above add-on will be overwritten by the install below, and removed
    // at the end of this task.

    let win = await BrowserTestUtils.openNewBrowserWindow();
    await SimpleTest.promiseFocus(win);

    let panelEventPromise = new Promise(resolve => {
      win.PopupNotifications.panel.addEventListener(
        "PanelUpdated",
        function eventListener(e) {
          if (e.detail.includes("addon-webext-permissions")) {
            win.PopupNotifications.panel.removeEventListener(
              "PanelUpdated",
              eventListener
            );
            // NOTE: the panel may intermittently still be in the "showing" state, and
            // so we explicitly await for the state to become "open" before proceeding
            // with asserting the visibility of the elements we expected to be in the
            // panel.
            if (win.PopupNotifications.panel.state === "showing") {
              TestUtils.waitForCondition(
                () => win.PopupNotifications.panel.state === "open",
                "Wait for addon-webext-permissions panel state to become open"
              ).then(resolve);
            } else {
              is(
                win.PopupNotifications.panel.state,
                "open",
                "Expect addon-webext-permissions panel state to be open"
              );
              resolve();
            }
          }
        }
      );
    });

    win.gBrowser.selectedTab = BrowserTestUtils.addTab(
      win.gBrowser,
      "about:blank"
    );
    await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser, {
      wantLoad: "about:blank",
    });
    win.gURLBar.value = TESTROOT + "amosigned.xpi";
    win.gURLBar.focus();
    EventUtils.synthesizeKey("KEY_Enter", {}, win);

    await panelEventPromise;
    await TestUtils.waitForTick();

    let panel = win.PopupNotifications.panel;
    let installDialog = panel.childNodes[0];

    testInstallDialogIncognitoCheckbox(installDialog, {
      incognitoHidden: false,
      incognitoChecked: true,
      toggleIncognito: true,
    });

    let notificationPromise = acceptAppMenuNotificationWhenShown(
      "addon-installed",
      "amosigned-xpi@tests.mozilla.org",
      { global: win }
    );
    let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
      "amosigned-xpi@tests.mozilla.org"
    );
    acceptInstallDialog(installDialog);
    await notificationPromise;

    let installs = await AddonManager.getAllInstalls();
    is(installs.length, 0, "Should be no pending installs");

    let addon = await AddonManager.getAddonByID(
      "amosigned-xpi@tests.mozilla.org"
    );

    await readyPromise;

    // This addon should no longer have private browsing permission.
    let policy = WebExtensionPolicy.getByID(addon.id);
    ok(!policy.privateBrowsingAllowed, "private browsing permission removed");

    await addon.uninstall();

    await BrowserTestUtils.closeWindow(win);
  });

  it("should fail to install an MV3 add-on without matching install_origins when extensions.install_origins.enabled=true", async function test_mv3_installOrigins_disallowed_with_unified_extensions() {
    await SpecialPowers.pushPrefEnv({
      set: [
        // Disable signature check because we load an unsigned MV3 extension.
        ["xpinstall.signatures.required", false],
        ["extensions.install_origins.enabled", true],
      ],
    });

    let win = await BrowserTestUtils.openNewBrowserWindow();
    await SimpleTest.promiseFocus(win);

    let notificationPromise = waitForNotification(
      "addon-install-failed",
      1,
      "unified-extensions-button",
      win
    );
    // This XPI does not have any `install_origins` in its manifest.
    let xpiURL = TESTROOT + "unsigned_mv3.xpi";
    await BrowserTestUtils.openNewForegroundTab(
      win.gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    await notificationPromise;

    await BrowserTestUtils.closeWindow(win);
    await SpecialPowers.popPrefEnv();
  });

  it("should allow installing an MV3 add-on without matching install_origins when extensions.install_origins.enabled=false", async function test_mv3_installOrigins_allowed_with_unified_extensions() {
    await SpecialPowers.pushPrefEnv({
      set: [
        // Disable signature check because we load an unsigned MV3 extension.
        ["xpinstall.signatures.required", false],
        // When this pref is disabled, install should be possible.
        ["extensions.install_origins.enabled", false],
      ],
    });

    let win = await BrowserTestUtils.openNewBrowserWindow();
    await SimpleTest.promiseFocus(win);

    let notificationPromise = waitForNotification(
      "addon-install-blocked",
      1,
      "unified-extensions-button",
      win
    );
    // This XPI does not have any `install_origins` in its manifest.
    let xpiURL = TESTROOT + "unsigned_mv3.xpi";
    await BrowserTestUtils.openNewForegroundTab(
      win.gBrowser,
      TESTROOT + "navigate.html?" + xpiURL
    );
    let panel = await notificationPromise;

    let closePromise = waitForNotificationClose(win);
    // hide the panel (this simulates the user dismissing it)
    panel.hidePopup();
    await closePromise;

    await BrowserTestUtils.closeWindow(win);
    await SpecialPowers.popPrefEnv();
  });
});
