tor-browser

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

head.js (23856B)


      1 ChromeUtils.defineESModuleGetters(this, {
      2  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
      3  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
      4  ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
      5 });
      6 
      7 const BASE = getRootDirectory(gTestPath).replace(
      8  "chrome://mochitests/content/",
      9  "https://example.com/"
     10 );
     11 
     12 ChromeUtils.defineLazyGetter(this, "Management", () => {
     13  return ExtensionParent.apiManager;
     14 });
     15 
     16 let { CustomizableUITestUtils } = ChromeUtils.importESModule(
     17  "resource://testing-common/CustomizableUITestUtils.sys.mjs"
     18 );
     19 let gCUITestUtils = new CustomizableUITestUtils(window);
     20 
     21 const { PermissionTestUtils } = ChromeUtils.importESModule(
     22  "resource://testing-common/PermissionTestUtils.sys.mjs"
     23 );
     24 
     25 let extL10n = null;
     26 /**
     27 * @param {string} id
     28 * @param {object} [args]
     29 * @returns {string}
     30 */
     31 function formatExtValue(id, args) {
     32  if (!extL10n) {
     33    extL10n = new Localization(
     34      [
     35        "toolkit/global/extensions.ftl",
     36        "toolkit/global/extensionPermissions.ftl",
     37        "branding/brand.ftl",
     38      ],
     39      true
     40    );
     41  }
     42  return extL10n.formatValueSync(id, args);
     43 }
     44 
     45 /**
     46 * Wait for the given PopupNotification to display
     47 *
     48 * @param {string} name
     49 *        The name of the notification to wait for.
     50 *
     51 * @returns {Promise}
     52 *          Resolves with the notification window.
     53 */
     54 function promisePopupNotificationShown(name) {
     55  return new Promise(resolve => {
     56    function popupshown() {
     57      let notification = PopupNotifications.getNotification(name);
     58      if (!notification) {
     59        return;
     60      }
     61 
     62      ok(notification, `${name} notification shown`);
     63      ok(PopupNotifications.isPanelOpen, "notification panel open");
     64 
     65      PopupNotifications.panel.removeEventListener("popupshown", popupshown);
     66      resolve(PopupNotifications.panel.firstElementChild);
     67    }
     68 
     69    PopupNotifications.panel.addEventListener("popupshown", popupshown);
     70  });
     71 }
     72 
     73 function promiseAppMenuNotificationShown(id) {
     74  const { AppMenuNotifications } = ChromeUtils.importESModule(
     75    "resource://gre/modules/AppMenuNotifications.sys.mjs"
     76  );
     77  return new Promise(resolve => {
     78    function popupshown() {
     79      let notification = AppMenuNotifications.activeNotification;
     80      if (!notification) {
     81        return;
     82      }
     83 
     84      is(notification.id, id, `${id} notification shown`);
     85      ok(PanelUI.isNotificationPanelOpen, "notification panel open");
     86 
     87      PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);
     88 
     89      let popupnotificationID = PanelUI._getPopupId(notification);
     90      let popupnotification = document.getElementById(popupnotificationID);
     91 
     92      resolve(popupnotification);
     93    }
     94    PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
     95  });
     96 }
     97 
     98 /**
     99 * Wait for a specific install event to fire for a given addon
    100 *
    101 * @param {AddonWrapper} addon
    102 *        The addon to watch for an event on
    103 * @param {string}
    104 *        The name of the event to watch for (e.g., onInstallEnded)
    105 *
    106 * @returns {Promise}
    107 *          Resolves when the event triggers with the first argument
    108 *          to the event handler as the resolution value.
    109 */
    110 function promiseInstallEvent(addon, event) {
    111  return new Promise(resolve => {
    112    let listener = {};
    113    listener[event] = (install, arg) => {
    114      if (install.addon.id == addon.id) {
    115        AddonManager.removeInstallListener(listener);
    116        resolve(arg);
    117      }
    118    };
    119    AddonManager.addInstallListener(listener);
    120  });
    121 }
    122 
    123 /**
    124 * Install an (xpi packaged) extension
    125 *
    126 * @param {string} url
    127 *        URL of the .xpi file to install
    128 * @param {object?} installTelemetryInfo
    129 *        an optional object that contains additional details used by the telemetry events.
    130 *
    131 * @returns {Promise}
    132 *          Resolves when the extension has been installed with the Addon
    133 *          object as the resolution value.
    134 */
    135 async function promiseInstallAddon(url, telemetryInfo) {
    136  let install = await AddonManager.getInstallForURL(url, { telemetryInfo });
    137  install.install();
    138 
    139  let addon = await new Promise(resolve => {
    140    install.addListener({
    141      onInstallEnded(_install, _addon) {
    142        resolve(_addon);
    143      },
    144    });
    145  });
    146 
    147  if (addon.isWebExtension) {
    148    await new Promise(resolve => {
    149      function listener(event, extension) {
    150        if (extension.id == addon.id) {
    151          Management.off("ready", listener);
    152          resolve();
    153        }
    154      }
    155      Management.on("ready", listener);
    156    });
    157  }
    158 
    159  return addon;
    160 }
    161 
    162 /**
    163 * Wait for an update to the given webextension to complete.
    164 * (This does not actually perform an update, it just watches for
    165 * the events that occur as a result of an update.)
    166 *
    167 * @param {AddonWrapper} addon
    168 *        The addon to be updated.
    169 *
    170 * @returns {Promise}
    171 *          Resolves when the extension has ben updated.
    172 */
    173 async function waitForUpdate(addon) {
    174  let installPromise = promiseInstallEvent(addon, "onInstallEnded");
    175  let readyPromise = new Promise(resolve => {
    176    function listener(event, extension) {
    177      if (extension.id == addon.id) {
    178        Management.off("ready", listener);
    179        resolve();
    180      }
    181    }
    182    Management.on("ready", listener);
    183  });
    184 
    185  let [newAddon] = await Promise.all([installPromise, readyPromise]);
    186  return newAddon;
    187 }
    188 
    189 function waitAboutAddonsViewLoaded(doc) {
    190  return BrowserTestUtils.waitForEvent(doc, "view-loaded");
    191 }
    192 
    193 /**
    194 * Trigger an action from the page options menu.
    195 */
    196 function triggerPageOptionsAction(win, action) {
    197  win.document.querySelector(`#page-options [action="${action}"]`).click();
    198 }
    199 
    200 function isDefaultIcon(icon) {
    201  return icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg";
    202 }
    203 
    204 /**
    205 * Check the contents of a permission popup notification
    206 *
    207 * @param {Window} panel
    208 *        The popup window.
    209 * @param {string|regexp|function} checkIcon
    210 *        The icon expected to appear in the notification.  If this is a
    211 *        string, it must match the icon url exactly.  If it is a
    212 *        regular expression it is tested against the icon url, and if
    213 *        it is a function, it is called with the icon url and returns
    214 *        true if the url is correct.
    215 * @param {Array} permissions
    216 *        The expected entries in the permissions list.  Each element
    217 *        in this array is itself a 2-element array with the string key
    218 *        for the item (e.g., "webext-perms-description-foo") and an
    219 *        optional formatting parameter.
    220 * @param {boolean} sideloaded
    221 *        Whether the notification is for a sideloaded extenion.
    222 */
    223 function checkNotification(panel, checkIcon, permissions, sideloaded) {
    224  let icon = panel.getAttribute("icon");
    225  let learnMoreLink = panel.querySelector(".popup-notification-learnmore-link");
    226  let listRequired = document.getElementById("addon-webext-perm-list-required");
    227  let listOptional = document.getElementById("addon-webext-perm-list-optional");
    228 
    229  if (checkIcon instanceof RegExp) {
    230    ok(
    231      checkIcon.test(icon),
    232      `Notification icon is correct ${JSON.stringify(icon)} ~= ${checkIcon}`
    233    );
    234  } else if (typeof checkIcon == "function") {
    235    ok(checkIcon(icon), "Notification icon is correct");
    236  } else {
    237    is(icon, checkIcon, "Notification icon is correct");
    238  }
    239 
    240  let description = panel.querySelector(
    241    ".popup-notification-description"
    242  ).textContent;
    243  let descL10nId = "webext-perms-header2";
    244  if (sideloaded) {
    245    descL10nId = "webext-perms-sideload-header";
    246  }
    247  const exp = formatExtValue(descL10nId, { extension: "<>" }).split("<>");
    248  ok(description.startsWith(exp.at(0)), "Description is the expected one");
    249  ok(description.endsWith(exp.at(-1)), "Description is the expected one");
    250 
    251  const hasPBCheckbox = !!listOptional.querySelector(
    252    "li.webext-perm-privatebrowsing > moz-checkbox"
    253  );
    254 
    255  is(
    256    BrowserTestUtils.isHidden(learnMoreLink),
    257    !permissions.length && !hasPBCheckbox,
    258    "Permissions learn more is hidden if there are no permissions and no private browsing checkbox"
    259  );
    260 
    261  if (!permissions.length && !hasPBCheckbox) {
    262    ok(listRequired.hidden, "Required permissions list is hidden");
    263    ok(listOptional.hidden, "Optional permissions list is hidden");
    264  } else if (!permissions.length) {
    265    ok(listRequired.hidden, "Required permissions list is hidden");
    266    ok(!listOptional.hidden, "Optional permissions list is visible");
    267    ok(hasPBCheckbox, "Expect a checkbox inside the list of permissions");
    268    is(
    269      listOptional.childElementCount,
    270      1,
    271      "Optional permissions list should have an entry"
    272    );
    273  } else if (permissions.length === 1 && hasPBCheckbox) {
    274    ok(!listRequired.hidden, "Required permissions list is visible");
    275    is(
    276      listRequired.childElementCount,
    277      1,
    278      "Required permissions list should have an entry"
    279    );
    280    ok(!listOptional.hidden, "Optional permissions list is visible");
    281    is(
    282      listOptional.childElementCount,
    283      1,
    284      "Optional permissions list should have an entry"
    285    );
    286    is(
    287      listRequired.children[0].textContent,
    288      formatExtValue(permissions[0]),
    289      "First Permission entry is correct"
    290    );
    291    const entry = listOptional.firstChild;
    292    ok(
    293      entry.classList.contains("webext-perm-privatebrowsing"),
    294      "Expect last permissions list entry to be the private browsing checkbox"
    295    );
    296    ok(
    297      entry.querySelector("moz-checkbox"),
    298      "Expect a checkbox inside the last permissions list entry"
    299    );
    300  } else {
    301    ok(!listRequired.hidden, "Required permissions list is visible");
    302    for (let i in permissions) {
    303      let [key, param] = permissions[i];
    304      const expected = formatExtValue(key, param);
    305      // If the permissions list entry has a label child element then
    306      // we expect the permission string to be set as the label element
    307      // value (in particular this is the case when the permission dialog
    308      // is going to show multiple host permissions as a single permission
    309      // entry and a nested ul listing all those domains).
    310      const permDescriptionEl = listRequired.children[i].querySelector("label")
    311        ? listRequired.children[i].firstElementChild.value
    312        : listRequired.children[i].textContent;
    313      is(permDescriptionEl, expected, `Permission number ${i + 1} is correct`);
    314    }
    315 
    316    if (hasPBCheckbox) {
    317      ok(!listOptional.hidden, "Optional permissions list is visible");
    318      const entry = listOptional.firstChild;
    319      ok(
    320        entry.classList.contains("webext-perm-privatebrowsing"),
    321        "Expect last permissions list entry to be the private browsing checkbox"
    322      );
    323    } else {
    324      ok(listOptional.hidden, "Optional permissions list is hidden");
    325    }
    326  }
    327 }
    328 
    329 /**
    330 * Test that install-time permission prompts work for a given
    331 * installation method.
    332 *
    333 * @param {Function} installFn
    334 *        Callable that takes the name of an xpi file to install and
    335 *        starts to install it.  Should return a Promise that resolves
    336 *        when the install is finished or rejects if the install is canceled.
    337 * @param {string} telemetryBase
    338 *        If supplied, the base type for telemetry events that should be
    339 *        recorded for this install method.
    340 *
    341 * @returns {Promise}
    342 */
    343 async function testInstallMethod(installFn) {
    344  const PERMS_XPI = "browser_webext_permissions.xpi";
    345  const NO_PERMS_XPI = "browser_webext_nopermissions.xpi";
    346  const ID = "permissions@test.mozilla.org";
    347 
    348  await SpecialPowers.pushPrefEnv({
    349    set: [
    350      ["extensions.webapi.testing", true],
    351      ["extensions.install.requireBuiltInCerts", false],
    352    ],
    353  });
    354 
    355  let testURI = makeURI("https://example.com/");
    356  PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
    357  registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
    358 
    359  async function runOnce(filename, cancel) {
    360    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
    361 
    362    let installPromise = new Promise(resolve => {
    363      let listener = {
    364        onDownloadCancelled() {
    365          AddonManager.removeInstallListener(listener);
    366          resolve(false);
    367        },
    368 
    369        onDownloadFailed() {
    370          AddonManager.removeInstallListener(listener);
    371          resolve(false);
    372        },
    373 
    374        onInstallCancelled() {
    375          AddonManager.removeInstallListener(listener);
    376          resolve(false);
    377        },
    378 
    379        onInstallEnded() {
    380          AddonManager.removeInstallListener(listener);
    381          resolve(true);
    382        },
    383 
    384        onInstallFailed() {
    385          AddonManager.removeInstallListener(listener);
    386          resolve(false);
    387        },
    388      };
    389      AddonManager.addInstallListener(listener);
    390    });
    391 
    392    let installMethodPromise = installFn(filename);
    393 
    394    let panel = await promisePopupNotificationShown("addon-webext-permissions");
    395    if (filename == PERMS_XPI) {
    396      const hostPermissions = [
    397        ["webext-perms-host-description-multiple-domains", { domainCount: 2 }],
    398      ];
    399 
    400      // The icon should come from the extension, don't bother with the precise
    401      // path, just make sure we've got a jar url pointing to the right path
    402      // inside the jar.
    403      checkNotification(panel, /^jar:file:\/\/.*\/icon\.png$/, [
    404        ...hostPermissions,
    405        ["webext-perms-description-nativeMessaging"],
    406        // The below permissions are deliberately in this order as permissions
    407        // are sorted alphabetically by the permission string to match AMO.
    408        ["webext-perms-description-history"],
    409        ["webext-perms-description-tabs"],
    410      ]);
    411    } else if (filename == NO_PERMS_XPI) {
    412      checkNotification(panel, isDefaultIcon, []);
    413    }
    414 
    415    if (cancel) {
    416      panel.secondaryButton.click();
    417      try {
    418        await installMethodPromise;
    419      } catch (err) {}
    420    } else {
    421      // Look for post-install notification
    422      let postInstallPromise =
    423        promiseAppMenuNotificationShown("addon-installed");
    424      panel.button.click();
    425 
    426      // Press OK on the post-install notification
    427      panel = await postInstallPromise;
    428      panel.button.click();
    429 
    430      await installMethodPromise;
    431    }
    432 
    433    let result = await installPromise;
    434    let addon = await AddonManager.getAddonByID(ID);
    435    if (cancel) {
    436      ok(!result, "Installation was cancelled");
    437      is(addon, null, "Extension is not installed");
    438    } else {
    439      ok(result, "Installation completed");
    440      isnot(addon, null, "Extension is installed");
    441      await addon.uninstall();
    442    }
    443 
    444    BrowserTestUtils.removeTab(tab);
    445  }
    446 
    447  // A few different tests for each installation method:
    448  // 1. Start installation of an extension that requests no permissions,
    449  //    verify the notification contents, then cancel the install
    450  await runOnce(NO_PERMS_XPI, true);
    451 
    452  // 2. Same as #1 but with an extension that requests some permissions.
    453  await runOnce(PERMS_XPI, true);
    454 
    455  // 3. Repeat with the same extension from step 2 but this time,
    456  //    accept the permissions to install the extension.  (Then uninstall
    457  //    the extension to clean up.)
    458  await runOnce(PERMS_XPI, false);
    459 
    460  await SpecialPowers.popPrefEnv();
    461 }
    462 
    463 // Helper function to test a specific scenario for interactive updates.
    464 // `checkFn` is a callable that triggers a check for updates.
    465 // `autoUpdate` specifies whether the test should be run with
    466 // updates applied automatically or not.
    467 async function interactiveUpdateTest(autoUpdate, checkFn) {
    468  AddonTestUtils.initMochitest(this);
    469  Services.fog.testResetFOG();
    470 
    471  const ID = "update2@tests.mozilla.org";
    472  const FAKE_INSTALL_SOURCE = "fake-install-source";
    473 
    474  await SpecialPowers.pushPrefEnv({
    475    set: [
    476      // We don't have pre-pinned certificates for the local mochitest server
    477      ["extensions.install.requireBuiltInCerts", false],
    478      ["extensions.update.requireBuiltInCerts", false],
    479 
    480      ["extensions.update.autoUpdateDefault", autoUpdate],
    481 
    482      // Point updates to the local mochitest server
    483      ["extensions.update.url", `${BASE}/browser_webext_update.json`],
    484    ],
    485  });
    486 
    487  AddonTestUtils.hookAMTelemetryEvents();
    488 
    489  // Trigger an update check, manually applying the update if we're testing
    490  // without auto-update.
    491  async function triggerUpdate(win, addon) {
    492    let manualUpdatePromise;
    493    if (!autoUpdate) {
    494      manualUpdatePromise = new Promise(resolve => {
    495        let listener = {
    496          onNewInstall() {
    497            AddonManager.removeInstallListener(listener);
    498            resolve();
    499          },
    500        };
    501        AddonManager.addInstallListener(listener);
    502      });
    503    }
    504 
    505    let promise = checkFn(win, addon);
    506 
    507    if (manualUpdatePromise) {
    508      await manualUpdatePromise;
    509 
    510      let doc = win.document;
    511      if (win.gViewController.currentViewId !== "addons://updates/available") {
    512        let showUpdatesBtn = doc.querySelector("addon-updates-message").button;
    513        await TestUtils.waitForCondition(() => {
    514          return !showUpdatesBtn.hidden;
    515        }, "Wait for show updates button");
    516        let viewChanged = waitAboutAddonsViewLoaded(doc);
    517        showUpdatesBtn.click();
    518        await viewChanged;
    519      }
    520      let card = await TestUtils.waitForCondition(() => {
    521        return doc.querySelector(`addon-card[addon-id="${ID}"]`);
    522      }, `Wait addon card for "${ID}"`);
    523      let updateBtn = card.querySelector('panel-item[action="install-update"]');
    524      ok(updateBtn, `Found update button for "${ID}"`);
    525      updateBtn.click();
    526    }
    527 
    528    return { promise };
    529  }
    530 
    531  // Navigate away from the starting page to force about:addons to load
    532  // in a new tab during the tests below.
    533  BrowserTestUtils.startLoadingURIString(
    534    gBrowser.selectedBrowser,
    535    "about:mozilla"
    536  );
    537  await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    538 
    539  // Install version 1.0 of the test extension
    540  let addon = await promiseInstallAddon(`${BASE}/browser_webext_update1.xpi`, {
    541    source: FAKE_INSTALL_SOURCE,
    542  });
    543  ok(addon, "Addon was installed");
    544  is(addon.version, "1.0", "Version 1 of the addon is installed");
    545 
    546  let win = await BrowserAddonUI.openAddonsMgr("addons://list/extension");
    547 
    548  await waitAboutAddonsViewLoaded(win.document);
    549 
    550  // Trigger an update check
    551  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
    552  let { promise: checkPromise } = await triggerUpdate(win, addon);
    553  let panel = await popupPromise;
    554 
    555  // Click the cancel button, wait to see the cancel event
    556  let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
    557  panel.secondaryButton.click();
    558  const cancelledByUser = await cancelPromise;
    559  is(cancelledByUser, true, "Install cancelled by user");
    560 
    561  addon = await AddonManager.getAddonByID(ID);
    562  is(addon.version, "1.0", "Should still be running the old version");
    563 
    564  // Make sure the update check is completely finished.
    565  await checkPromise;
    566 
    567  // Trigger a new update check
    568  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
    569  checkPromise = (await triggerUpdate(win, addon)).promise;
    570 
    571  // This time, accept the upgrade
    572  let updatePromise = waitForUpdate(addon);
    573  panel = await popupPromise;
    574  panel.button.click();
    575 
    576  addon = await updatePromise;
    577  is(addon.version, "2.0", "Should have upgraded");
    578 
    579  await checkPromise;
    580 
    581  BrowserTestUtils.removeTab(gBrowser.selectedTab);
    582  await addon.uninstall();
    583  await SpecialPowers.popPrefEnv();
    584 
    585  const collectedUpdateEvents = AddonTestUtils.getAMTelemetryEvents().filter(
    586    evt => {
    587      return evt.method === "update";
    588    }
    589  );
    590 
    591  const expectedSteps = [
    592    // First update is cancelled on the permission prompt.
    593    "started",
    594    "download_started",
    595    "download_completed",
    596    "permissions_prompt",
    597    "cancelled",
    598    // Second update is expected to be completed.
    599    "started",
    600    "download_started",
    601    "download_completed",
    602    "permissions_prompt",
    603    "completed",
    604  ];
    605 
    606  Assert.deepEqual(
    607    expectedSteps,
    608    collectedUpdateEvents.map(evt => evt.extra.step),
    609    "Got the expected sequence on update telemetry events"
    610  );
    611 
    612  let gleanEvents = AddonTestUtils.getAMGleanEvents("update");
    613  Services.fog.testResetFOG();
    614 
    615  Assert.deepEqual(
    616    expectedSteps,
    617    gleanEvents.map(e => e.step),
    618    "Got the expected sequence on update Glean events."
    619  );
    620 
    621  ok(
    622    collectedUpdateEvents.every(evt => evt.extra.addon_id === ID),
    623    "Every update telemetry event should have the expected addon_id extra var"
    624  );
    625 
    626  ok(
    627    collectedUpdateEvents.every(
    628      evt => evt.extra.source === FAKE_INSTALL_SOURCE
    629    ),
    630    "Every update telemetry event should have the expected source extra var"
    631  );
    632 
    633  ok(
    634    collectedUpdateEvents.every(evt => evt.extra.updated_from === "user"),
    635    "Every update telemetry event should have the update_from extra var 'user'"
    636  );
    637 
    638  for (let e of gleanEvents) {
    639    is(e.addon_id, ID, "Glean event has the expected addon_id.");
    640    is(e.source, FAKE_INSTALL_SOURCE, "Glean event has the expected source.");
    641    is(e.updated_from, "user", "Glean event has the expected updated_from.");
    642 
    643    if (e.step === "permissions_prompt") {
    644      Assert.greater(parseInt(e.num_strings), 0, "Expected num_strings.");
    645    }
    646    if (e.step === "download_completed") {
    647      Assert.greater(parseInt(e.download_time), 0, "Valid download_time.");
    648    }
    649  }
    650 
    651  let hasPermissionsExtras = collectedUpdateEvents
    652    .filter(evt => {
    653      return evt.extra.step === "permissions_prompt";
    654    })
    655    .every(evt => {
    656      return Number.isInteger(parseInt(evt.extra.num_strings, 10));
    657    });
    658 
    659  ok(
    660    hasPermissionsExtras,
    661    "Every 'permissions_prompt' update telemetry event should have the permissions extra vars"
    662  );
    663 
    664  let hasDownloadTimeExtras = collectedUpdateEvents
    665    .filter(evt => {
    666      return evt.extra.step === "download_completed";
    667    })
    668    .every(evt => {
    669      const download_time = parseInt(evt.extra.download_time, 10);
    670      return !isNaN(download_time) && download_time > 0;
    671    });
    672 
    673  ok(
    674    hasDownloadTimeExtras,
    675    "Every 'download_completed' update telemetry event should have a download_time extra vars"
    676  );
    677 }
    678 
    679 async function getCachedPermissions(extensionId) {
    680  const NotFound = Symbol("extension ID not found in permissions cache");
    681  try {
    682    return await ExtensionParent.StartupCache.permissions.get(
    683      extensionId,
    684      () => {
    685        // Throw error to prevent the key from being created.
    686        throw NotFound;
    687      }
    688    );
    689  } catch (e) {
    690    if (e === NotFound) {
    691      return null;
    692    }
    693    throw e;
    694  }
    695 }
    696 
    697 // The tests in this directory install a bunch of extensions but they
    698 // need to uninstall them before exiting, as a stray leftover extension
    699 // after one test can foul up subsequent tests.
    700 // So, add a task to run before any tests that grabs a list of all the
    701 // add-ons that are pre-installed in the test environment and then checks
    702 // the list of installed add-ons at the end of the test to make sure no
    703 // new add-ons have been added.
    704 // Individual tests can store a cleanup function in the testCleanup global
    705 // to ensure it gets called before the final check is performed.
    706 let testCleanup;
    707 add_setup(async function head_setup() {
    708  let addons = await AddonManager.getAllAddons();
    709  let existingAddons = new Set(addons.map(a => a.id));
    710 
    711  let uuids = Services.prefs.getStringPref("extensions.webextensions.uuids");
    712 
    713  registerCleanupFunction(async function () {
    714    if (testCleanup) {
    715      await testCleanup();
    716      testCleanup = null;
    717    }
    718 
    719    for (let addon of await AddonManager.getAllAddons()) {
    720      if (!existingAddons.has(addon.id)) {
    721        ok(
    722          false,
    723          `Addon ${addon.id} was left installed at the end of the test`
    724        );
    725        await addon.uninstall();
    726      }
    727    }
    728    // Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1974419
    729    is(
    730      Services.prefs.getStringPref("extensions.webextensions.uuids"),
    731      uuids,
    732      "No unexpected changes to extensions.webextensions.uuid"
    733    );
    734  });
    735 });