tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_midi_permission_gated.js (24854B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const EXAMPLE_COM_URL =
      5  "https://example.com/document-builder.sjs?html=<h1>Test midi permission with synthetic site permission addon</h1>";
      6 const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html=
      7  <h1>Test midi permission with synthetic site permission addon in iframes</h1>
      8  <iframe id=sameOrigin src="${encodeURIComponent(
      9    'https://example.org/document-builder.sjs?html=SameOrigin"'
     10  )}"></iframe>
     11  <iframe id=crossOrigin  src="${encodeURIComponent(
     12    'https://example.net/document-builder.sjs?html=CrossOrigin"'
     13  )}"></iframe>`;
     14 
     15 const l10n = new Localization(
     16  [
     17    "browser/addonNotifications.ftl",
     18    "toolkit/global/extensions.ftl",
     19    "toolkit/global/extensionPermissions.ftl",
     20    "branding/brand.ftl",
     21  ],
     22  true
     23 );
     24 
     25 const { HttpServer } = ChromeUtils.importESModule(
     26  "resource://testing-common/httpd.sys.mjs"
     27 );
     28 ChromeUtils.defineESModuleGetters(this, {
     29  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
     30 });
     31 
     32 add_setup(async function () {
     33  await SpecialPowers.pushPrefEnv({
     34    set: [["midi.prompt.testing", false]],
     35  });
     36 
     37  AddonTestUtils.initMochitest(this);
     38  AddonTestUtils.hookAMTelemetryEvents();
     39 
     40  // Once the addon is installed, a dialog is displayed as a confirmation.
     41  // This could interfere with tests running after this one, so we set up a listener
     42  // that will always accept post install dialogs so we don't have  to deal with them in
     43  // the test.
     44  alwaysAcceptAddonPostInstallDialogs();
     45 
     46  registerCleanupFunction(async () => {
     47    // Remove the permission.
     48    await SpecialPowers.removePermission("midi-sysex", {
     49      url: EXAMPLE_COM_URL,
     50    });
     51    await SpecialPowers.removePermission("midi-sysex", {
     52      url: PAGE_WITH_IFRAMES_URL,
     53    });
     54    await SpecialPowers.removePermission("midi", {
     55      url: EXAMPLE_COM_URL,
     56    });
     57    await SpecialPowers.removePermission("midi", {
     58      url: PAGE_WITH_IFRAMES_URL,
     59    });
     60    await SpecialPowers.removePermission("install", {
     61      url: EXAMPLE_COM_URL,
     62    });
     63 
     64    while (gBrowser.tabs.length > 1) {
     65      BrowserTestUtils.removeTab(gBrowser.selectedTab);
     66    }
     67  });
     68 });
     69 
     70 add_task(async function testRequestMIDIAccess() {
     71  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL);
     72  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
     73  const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host;
     74  Services.fog.testResetFOG();
     75 
     76  info("Check that midi-sysex isn't set");
     77  ok(
     78    await SpecialPowers.testPermission(
     79      "midi-sysex",
     80      SpecialPowers.Services.perms.UNKNOWN_ACTION,
     81      { url: EXAMPLE_COM_URL }
     82    ),
     83    "midi-sysex value should have UNKNOWN permission"
     84  );
     85 
     86  info("Request midi-sysex access");
     87  let onAddonInstallBlockedNotification = waitForNotification(
     88    "addon-install-blocked"
     89  );
     90  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
     91    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
     92      sysex: true,
     93    });
     94  });
     95 
     96  info("Deny site permission addon install in first popup");
     97  let addonInstallPanel = await onAddonInstallBlockedNotification;
     98  const [installPopupHeader, installPopupMessage] =
     99    addonInstallPanel.querySelectorAll(
    100      "description.popup-notification-description"
    101    );
    102  is(
    103    installPopupHeader.textContent,
    104    l10n.formatValueSync("site-permission-install-first-prompt-midi-header"),
    105    "First popup has expected header text"
    106  );
    107  is(
    108    installPopupMessage.textContent,
    109    l10n.formatValueSync("site-permission-install-first-prompt-midi-message"),
    110    "First popup has expected message"
    111  );
    112 
    113  let notification = addonInstallPanel.childNodes[0];
    114  // secondaryButton is the "Don't allow" button
    115  notification.secondaryButton.click();
    116 
    117  let rejectionMessage = await SpecialPowers.spawn(
    118    gBrowser.selectedBrowser,
    119    [],
    120    async () => {
    121      let errorMessage;
    122      try {
    123        await content.midiAccessRequestPromise;
    124      } catch (e) {
    125        errorMessage = `${e.name}: ${e.message}`;
    126      }
    127 
    128      delete content.midiAccessRequestPromise;
    129      return errorMessage;
    130    }
    131  );
    132  is(
    133    rejectionMessage,
    134    "SecurityError: WebMIDI requires a site permission add-on to activate"
    135  );
    136 
    137  assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]);
    138 
    139  info("Deny site permission addon install in second popup");
    140  onAddonInstallBlockedNotification = waitForNotification(
    141    "addon-install-blocked"
    142  );
    143  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    144    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
    145      sysex: true,
    146    });
    147  });
    148  addonInstallPanel = await onAddonInstallBlockedNotification;
    149  notification = addonInstallPanel.childNodes[0];
    150  let dialogPromise = waitForInstallDialog();
    151  notification.button.click();
    152  let installDialog = await dialogPromise;
    153  is(
    154    installDialog.querySelector(".popup-notification-description").textContent,
    155    l10n.formatValueSync(
    156      "webext-site-perms-header-with-gated-perms-midi-sysex",
    157      { hostname: testPageHost }
    158    ),
    159    "Install dialog has expected header text"
    160  );
    161  is(
    162    installDialog.querySelector("popupnotificationcontent description")
    163      .textContent,
    164    l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
    165    "Install dialog has expected description"
    166  );
    167 
    168  // secondaryButton is the "Cancel" button
    169  installDialog.secondaryButton.click();
    170 
    171  rejectionMessage = await SpecialPowers.spawn(
    172    gBrowser.selectedBrowser,
    173    [],
    174    async () => {
    175      let errorMessage;
    176      try {
    177        await content.midiAccessRequestPromise;
    178      } catch (e) {
    179        errorMessage = `${e.name}: ${e.message}`;
    180      }
    181 
    182      delete content.midiAccessRequestPromise;
    183      return errorMessage;
    184    }
    185  );
    186  is(
    187    rejectionMessage,
    188    "SecurityError: WebMIDI requires a site permission add-on to activate"
    189  );
    190 
    191  assertSitePermissionInstallTelemetryEvents([
    192    "site_warning",
    193    "permissions_prompt",
    194    "cancelled",
    195  ]);
    196 
    197  info("Request midi-sysex access again");
    198  onAddonInstallBlockedNotification = waitForNotification(
    199    "addon-install-blocked"
    200  );
    201  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    202    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
    203      sysex: true,
    204    });
    205  });
    206 
    207  info("Accept site permission addon install");
    208  addonInstallPanel = await onAddonInstallBlockedNotification;
    209  notification = addonInstallPanel.childNodes[0];
    210  dialogPromise = waitForInstallDialog();
    211  notification.button.click();
    212  installDialog = await dialogPromise;
    213  installDialog.button.click();
    214 
    215  info("Wait for the midi-sysex access request promise to resolve");
    216  let accessGranted = await SpecialPowers.spawn(
    217    gBrowser.selectedBrowser,
    218    [],
    219    async () => {
    220      try {
    221        await content.midiAccessRequestPromise;
    222        return true;
    223      } catch (e) {}
    224 
    225      delete content.midiAccessRequestPromise;
    226      return false;
    227    }
    228  );
    229  ok(accessGranted, "requestMIDIAccess resolved");
    230 
    231  info("Check that midi-sysex is now set");
    232  ok(
    233    await SpecialPowers.testPermission(
    234      "midi-sysex",
    235      SpecialPowers.Services.perms.ALLOW_ACTION,
    236      { url: EXAMPLE_COM_URL }
    237    ),
    238    "midi-sysex value should have ALLOW permission"
    239  );
    240  ok(
    241    await SpecialPowers.testPermission(
    242      "midi",
    243      SpecialPowers.Services.perms.UNKNOWN_ACTION,
    244      { url: EXAMPLE_COM_URL }
    245    ),
    246    "but midi should have UNKNOWN permission"
    247  );
    248 
    249  info("Check that we don't prompt user again once they installed the addon");
    250  const accessPromiseState = await SpecialPowers.spawn(
    251    gBrowser.selectedBrowser,
    252    [],
    253    async () => {
    254      return content.navigator
    255        .requestMIDIAccess({ sysex: true })
    256        .then(() => "resolved");
    257    }
    258  );
    259  is(
    260    accessPromiseState,
    261    "resolved",
    262    "requestMIDIAccess resolved without user prompt"
    263  );
    264 
    265  assertSitePermissionInstallTelemetryEvents([
    266    "site_warning",
    267    "permissions_prompt",
    268    "completed",
    269  ]);
    270 
    271  info("Request midi access without sysex");
    272  onAddonInstallBlockedNotification = waitForNotification(
    273    "addon-install-blocked"
    274  );
    275  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    276    content.midiNoSysexAccessRequestPromise =
    277      content.navigator.requestMIDIAccess();
    278  });
    279 
    280  info("Accept site permission addon install");
    281  addonInstallPanel = await onAddonInstallBlockedNotification;
    282  notification = addonInstallPanel.childNodes[0];
    283 
    284  is(
    285    notification
    286      .querySelector("#addon-install-blocked-info")
    287      .getAttribute("href"),
    288    Services.urlFormatter.formatURLPref("app.support.baseURL") +
    289      "site-permission-addons",
    290    "Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
    291  );
    292 
    293  dialogPromise = waitForInstallDialog();
    294  notification.button.click();
    295  installDialog = await dialogPromise;
    296 
    297  is(
    298    installDialog.querySelector(".popup-notification-description").textContent,
    299    l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", {
    300      hostname: testPageHost,
    301    }),
    302    "Install dialog has expected header text"
    303  );
    304  is(
    305    installDialog.querySelector("popupnotificationcontent description")
    306      .textContent,
    307    l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
    308    "Install dialog has expected description"
    309  );
    310 
    311  installDialog.button.click();
    312 
    313  info("Wait for the midi access request promise to resolve");
    314  accessGranted = await SpecialPowers.spawn(
    315    gBrowser.selectedBrowser,
    316    [],
    317    async () => {
    318      try {
    319        await content.midiNoSysexAccessRequestPromise;
    320        return true;
    321      } catch (e) {}
    322 
    323      delete content.midiNoSysexAccessRequestPromise;
    324      return false;
    325    }
    326  );
    327  ok(accessGranted, "requestMIDIAccess resolved");
    328 
    329  info("Check that both midi-sysex and midi are now set");
    330  ok(
    331    await SpecialPowers.testPermission(
    332      "midi-sysex",
    333      SpecialPowers.Services.perms.ALLOW_ACTION,
    334      { url: EXAMPLE_COM_URL }
    335    ),
    336    "midi-sysex value should have ALLOW permission"
    337  );
    338  ok(
    339    await SpecialPowers.testPermission(
    340      "midi",
    341      SpecialPowers.Services.perms.ALLOW_ACTION,
    342      { url: EXAMPLE_COM_URL }
    343    ),
    344    "and midi value should also have ALLOW permission"
    345  );
    346 
    347  assertSitePermissionInstallTelemetryEvents([
    348    "site_warning",
    349    "permissions_prompt",
    350    "completed",
    351  ]);
    352 
    353  info("Check that we don't prompt user again when they perm denied");
    354  // remove permission to have a clean state
    355  await SpecialPowers.removePermission("midi-sysex", {
    356    url: EXAMPLE_COM_URL,
    357  });
    358 
    359  onAddonInstallBlockedNotification = waitForNotification(
    360    "addon-install-blocked"
    361  );
    362  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    363    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
    364      sysex: true,
    365    });
    366  });
    367 
    368  info("Perm-deny site permission addon install");
    369  addonInstallPanel = await onAddonInstallBlockedNotification;
    370  // Click the "Report Suspicious Site" menuitem, which has the same effect as
    371  // "Never Allow" and also submits a telemetry event (which we check below).
    372  notification.menupopup.querySelectorAll("menuitem")[1].click();
    373 
    374  rejectionMessage = await SpecialPowers.spawn(
    375    gBrowser.selectedBrowser,
    376    [],
    377    async () => {
    378      let errorMessage;
    379      try {
    380        await content.midiAccessRequestPromise;
    381      } catch (e) {
    382        errorMessage = e.name;
    383      }
    384 
    385      delete content.midiAccessRequestPromise;
    386      return errorMessage;
    387    }
    388  );
    389  is(rejectionMessage, "SecurityError", "requestMIDIAccess was rejected");
    390 
    391  info("Request midi-sysex access again");
    392  let denyIntervalStart = performance.now();
    393  rejectionMessage = await SpecialPowers.spawn(
    394    gBrowser.selectedBrowser,
    395    [],
    396    async () => {
    397      let errorMessage;
    398      try {
    399        await content.navigator.requestMIDIAccess({
    400          sysex: true,
    401        });
    402      } catch (e) {
    403        errorMessage = e.name;
    404      }
    405      return errorMessage;
    406    }
    407  );
    408  is(
    409    rejectionMessage,
    410    "SecurityError",
    411    "requestMIDIAccess was rejected without user prompt"
    412  );
    413  let denyIntervalElapsed = performance.now() - denyIntervalStart;
    414  Assert.greaterOrEqual(
    415    denyIntervalElapsed,
    416    3000,
    417    `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${
    418      denyIntervalElapsed / 1000
    419    } seconds)`
    420  );
    421 
    422  Assert.deepEqual(
    423    [{ suspicious_site: "example.com" }],
    424    AddonTestUtils.getAMGleanEvents("reportSuspiciousSite"),
    425    "Expected Glean event recorded."
    426  );
    427 
    428  assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]);
    429 });
    430 
    431 add_task(async function testIframeRequestMIDIAccess() {
    432  gBrowser.selectedTab = BrowserTestUtils.addTab(
    433    gBrowser,
    434    PAGE_WITH_IFRAMES_URL
    435  );
    436  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    437 
    438  info("Check that midi-sysex isn't set");
    439  ok(
    440    await SpecialPowers.testPermission(
    441      "midi-sysex",
    442      SpecialPowers.Services.perms.UNKNOWN_ACTION,
    443      { url: PAGE_WITH_IFRAMES_URL }
    444    ),
    445    "midi-sysex value should have UNKNOWN permission"
    446  );
    447 
    448  info("Request midi-sysex access from the same-origin iframe");
    449  const sameOriginIframeBrowsingContext = await SpecialPowers.spawn(
    450    gBrowser.selectedBrowser,
    451    [],
    452    async () => {
    453      return content.document.getElementById("sameOrigin").browsingContext;
    454    }
    455  );
    456 
    457  let onAddonInstallBlockedNotification = waitForNotification(
    458    "addon-install-blocked"
    459  );
    460  await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => {
    461    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
    462      sysex: true,
    463    });
    464  });
    465 
    466  info("Accept site permission addon install");
    467  const addonInstallPanel = await onAddonInstallBlockedNotification;
    468  const notification = addonInstallPanel.childNodes[0];
    469  const dialogPromise = waitForInstallDialog();
    470  notification.button.click();
    471  let installDialog = await dialogPromise;
    472  installDialog.button.click();
    473 
    474  info("Wait for the midi-sysex access request promise to resolve");
    475  const accessGranted = await SpecialPowers.spawn(
    476    sameOriginIframeBrowsingContext,
    477    [],
    478    async () => {
    479      try {
    480        await content.midiAccessRequestPromise;
    481        return true;
    482      } catch (e) {}
    483 
    484      delete content.midiAccessRequestPromise;
    485      return false;
    486    }
    487  );
    488  ok(accessGranted, "requestMIDIAccess resolved");
    489 
    490  info("Check that midi-sysex is now set");
    491  ok(
    492    await SpecialPowers.testPermission(
    493      "midi-sysex",
    494      SpecialPowers.Services.perms.ALLOW_ACTION,
    495      { url: PAGE_WITH_IFRAMES_URL }
    496    ),
    497    "midi-sysex value should have ALLOW permission"
    498  );
    499 
    500  info(
    501    "Check that we don't prompt user again once they installed the addon from the same-origin iframe"
    502  );
    503  const accessPromiseState = await SpecialPowers.spawn(
    504    gBrowser.selectedBrowser,
    505    [],
    506    async () => {
    507      return content.navigator
    508        .requestMIDIAccess({ sysex: true })
    509        .then(() => "resolved");
    510    }
    511  );
    512  is(
    513    accessPromiseState,
    514    "resolved",
    515    "requestMIDIAccess resolved without user prompt"
    516  );
    517 
    518  assertSitePermissionInstallTelemetryEvents([
    519    "site_warning",
    520    "permissions_prompt",
    521    "completed",
    522  ]);
    523 
    524  info("Check that request is rejected when done from a cross-origin iframe");
    525  const crossOriginIframeBrowsingContext = await SpecialPowers.spawn(
    526    gBrowser.selectedBrowser,
    527    [],
    528    async () => {
    529      return content.document.getElementById("crossOrigin").browsingContext;
    530    }
    531  );
    532 
    533  const onConsoleErrorMessage = new Promise(resolve => {
    534    const errorListener = {
    535      observe(error) {
    536        if (error.message.includes("WebMIDI access request was denied")) {
    537          resolve(error);
    538          Services.console.unregisterListener(errorListener);
    539        }
    540      },
    541    };
    542    Services.console.registerListener(errorListener);
    543  });
    544 
    545  const rejectionMessage = await SpecialPowers.spawn(
    546    crossOriginIframeBrowsingContext,
    547    [],
    548    async () => {
    549      let errorName;
    550      try {
    551        await content.navigator.requestMIDIAccess({
    552          sysex: true,
    553        });
    554      } catch (e) {
    555        errorName = e.name;
    556      }
    557      return errorName;
    558    }
    559  );
    560 
    561  is(
    562    rejectionMessage,
    563    "SecurityError",
    564    "requestMIDIAccess from the remote iframe was rejected"
    565  );
    566 
    567  const consoleErrorMessage = await onConsoleErrorMessage;
    568  ok(
    569    consoleErrorMessage.message.includes(
    570      `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`,
    571      "an error message is sent to the console"
    572    )
    573  );
    574  assertSitePermissionInstallTelemetryEvents([]);
    575 });
    576 
    577 add_task(async function testRequestMIDIAccessLocalhost() {
    578  const httpServer = new HttpServer();
    579  httpServer.start(-1);
    580  httpServer.registerPathHandler(`/test`, function (request, response) {
    581    response.setStatusLine(request.httpVersion, 200, "OK");
    582    response.write(`
    583      <!DOCTYPE html>
    584      <meta charset=utf8>
    585      <h1>Test requestMIDIAccess on lcoalhost</h1>`);
    586  });
    587  const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`;
    588 
    589  registerCleanupFunction(async function cleanup() {
    590    await new Promise(resolve => httpServer.stop(resolve));
    591  });
    592 
    593  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl);
    594  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    595 
    596  info("Check that midi-sysex isn't set");
    597  ok(
    598    await SpecialPowers.testPermission(
    599      "midi-sysex",
    600      SpecialPowers.Services.perms.UNKNOWN_ACTION,
    601      { url: localHostTestUrl }
    602    ),
    603    "midi-sysex value should have UNKNOWN permission"
    604  );
    605 
    606  info(
    607    "Request midi-sysex access should not prompt for addon install on locahost, but for permission"
    608  );
    609  let popupShown = BrowserTestUtils.waitForEvent(
    610    PopupNotifications.panel,
    611    "popupshown"
    612  );
    613  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
    614    content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
    615      sysex: true,
    616    });
    617  });
    618  await popupShown;
    619  is(
    620    PopupNotifications.panel.querySelector("popupnotification").id,
    621    "midi-notification",
    622    "midi notification was displayed"
    623  );
    624 
    625  info("Accept permission");
    626  PopupNotifications.panel
    627    .querySelector(".popup-notification-primary-button")
    628    .click();
    629 
    630  info("Wait for the midi-sysex access request promise to resolve");
    631  let accessGranted = await SpecialPowers.spawn(
    632    gBrowser.selectedBrowser,
    633    [],
    634    async () => {
    635      try {
    636        await content.midiAccessRequestPromise;
    637        return true;
    638      } catch (e) {}
    639 
    640      delete content.midiAccessRequestPromise;
    641      return false;
    642    }
    643  );
    644  ok(accessGranted, "requestMIDIAccess resolved");
    645 
    646  // We're remembering permission grants temporarily on the tab since Bug 1754005.
    647  info(
    648    "Check that a new request is automatically granted because we granted before in the same tab."
    649  );
    650 
    651  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
    652    content.navigator.requestMIDIAccess({ sysex: true });
    653  });
    654 
    655  accessGranted = await SpecialPowers.spawn(
    656    gBrowser.selectedBrowser,
    657    [],
    658    async () => {
    659      try {
    660        await content.midiAccessRequestPromise;
    661        return true;
    662      } catch (e) {}
    663 
    664      delete content.midiAccessRequestPromise;
    665      return false;
    666    }
    667  );
    668  ok(accessGranted, "requestMIDIAccess resolved");
    669 
    670  assertSitePermissionInstallTelemetryEvents([]);
    671 });
    672 
    673 add_task(async function testDisabledRequestMIDIAccessFile() {
    674  let dir = getChromeDir(getResolvedURI(gTestPath));
    675  dir.append("blank.html");
    676  const fileSchemeTestUri = Services.io.newFileURI(dir).spec;
    677 
    678  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri);
    679  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    680 
    681  info("Check that requestMIDIAccess isn't set on navigator on file scheme");
    682  const isRequestMIDIAccessDefined = await SpecialPowers.spawn(
    683    gBrowser.selectedBrowser,
    684    [],
    685    () => {
    686      return "requestMIDIAccess" in content.wrappedJSObject.navigator;
    687    }
    688  );
    689  is(
    690    isRequestMIDIAccessDefined,
    691    false,
    692    "navigator.requestMIDIAccess is not defined on file scheme"
    693  );
    694 });
    695 
    696 // Ignore any additional telemetry events collected in this file.
    697 // Unfortunately it doesn't work to have this in a cleanup function.
    698 // Keep this as the last task done.
    699 add_task(function teardown_telemetry_events() {
    700  AddonTestUtils.getAMTelemetryEvents();
    701 });
    702 
    703 /**
    704 *  Check that the expected sitepermission install events are recorded.
    705 *
    706 * @param {Array<string>} expectedSteps: An array of the expected extra.step values recorded.
    707 */
    708 function assertSitePermissionInstallTelemetryEvents(expectedSteps) {
    709  let amInstallEvents = AddonTestUtils.getAMTelemetryEvents()
    710    .filter(evt => evt.method === "install" && evt.object === "sitepermission")
    711    .map(evt => evt.extra.step);
    712 
    713  Assert.deepEqual(amInstallEvents, expectedSteps);
    714 }
    715 
    716 async function waitForInstallDialog(id = "addon-webext-permissions") {
    717  let panel = await waitForNotification(id);
    718  return panel.childNodes[0];
    719 }
    720 
    721 /**
    722 * Adds an event listener that will listen for post-install dialog event and automatically
    723 * close the dialogs.
    724 */
    725 function alwaysAcceptAddonPostInstallDialogs() {
    726  // Once the addon is installed, a dialog is displayed as a confirmation.
    727  // This could interfere with tests running after this one, so we set up a listener
    728  // that will always accept post install dialogs so we don't have  to deal with them in
    729  // the test.
    730  const abortController = new AbortController();
    731 
    732  const { AppMenuNotifications } = ChromeUtils.importESModule(
    733    "resource://gre/modules/AppMenuNotifications.sys.mjs"
    734  );
    735  info("Start listening and accept addon post-install notifications");
    736  PanelUI.notificationPanel.addEventListener(
    737    "popupshown",
    738    async function popupshown() {
    739      let notification = AppMenuNotifications.activeNotification;
    740      if (!notification || notification.id !== "addon-installed") {
    741        return;
    742      }
    743 
    744      let popupnotificationID = PanelUI._getPopupId(notification);
    745      if (popupnotificationID) {
    746        info("Accept post-install dialog");
    747        let popupnotification = document.getElementById(popupnotificationID);
    748        popupnotification?.button.click();
    749      }
    750    },
    751    {
    752      signal: abortController.signal,
    753    }
    754  );
    755 
    756  registerCleanupFunction(async () => {
    757    // Clear the listener at the end of the test file, to prevent it to stay
    758    // around when the same browser instance may be running other unrelated
    759    // test files.
    760    abortController.abort();
    761  });
    762 }
    763 
    764 const PROGRESS_NOTIFICATION = "addon-progress";
    765 async function waitForNotification(notificationId) {
    766  info(`Waiting for ${notificationId} notification`);
    767 
    768  let topic = getObserverTopic(notificationId);
    769 
    770  let observerPromise;
    771  if (notificationId !== "addon-webext-permissions") {
    772    observerPromise = new Promise(resolve => {
    773      Services.obs.addObserver(function observer(aSubject, aTopic) {
    774        // Ignore the progress notification unless that is the notification we want
    775        if (
    776          notificationId != PROGRESS_NOTIFICATION &&
    777          aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
    778        ) {
    779          return;
    780        }
    781        Services.obs.removeObserver(observer, topic);
    782        resolve();
    783      }, topic);
    784    });
    785  }
    786 
    787  let panelEventPromise = new Promise(resolve => {
    788    window.PopupNotifications.panel.addEventListener(
    789      "PanelUpdated",
    790      function eventListener(e) {
    791        // Skip notifications that are not the one that we are supposed to be looking for
    792        if (!e.detail.includes(notificationId)) {
    793          return;
    794        }
    795        window.PopupNotifications.panel.removeEventListener(
    796          "PanelUpdated",
    797          eventListener
    798        );
    799        resolve();
    800      }
    801    );
    802  });
    803 
    804  await observerPromise;
    805  await panelEventPromise;
    806  await waitForTick();
    807 
    808  info(`Saw a ${notificationId} notification`);
    809  await SimpleTest.promiseFocus(window.PopupNotifications.window);
    810  return window.PopupNotifications.panel;
    811 }
    812 
    813 // This function is similar to the one in
    814 // toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js,
    815 // please keep both in sync!
    816 function getObserverTopic(aNotificationId) {
    817  let topic = aNotificationId;
    818  if (topic == "xpinstall-disabled") {
    819    topic = "addon-install-disabled";
    820  } else if (topic == "addon-progress") {
    821    topic = "addon-install-started";
    822  } else if (topic == "addon-installed") {
    823    topic = "webextension-install-notify";
    824  }
    825  return topic;
    826 }
    827 
    828 function waitForTick() {
    829  return new Promise(resolve => executeSoon(resolve));
    830 }