tor-browser

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

browser_unified_extensions_empty_panel.js (24829B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { AddonTestUtils } = ChromeUtils.importESModule(
      7  "resource://testing-common/AddonTestUtils.sys.mjs"
      8 );
      9 AddonTestUtils.initMochitest(this);
     10 
     11 const { sinon } = ChromeUtils.importESModule(
     12  "resource://testing-common/Sinon.sys.mjs"
     13 );
     14 
     15 loadTestSubscript("head_unified_extensions.js");
     16 
     17 // The createExtensions helper (using ExtensionTestUtils.loadExtension) does
     18 // not support disabled add-ons. This helper uses AOM directly instead.
     19 async function promiseInstallWebExtension(extensionData) {
     20  let addonFile = AddonTestUtils.createTempWebExtensionFile(extensionData);
     21  let { addon } = await AddonTestUtils.promiseInstallFile(addonFile);
     22  return addon;
     23 }
     24 
     25 add_setup(async function () {
     26  // Make sure extension buttons added to the navbar will not overflow in the
     27  // panel, which could happen when a previous test file resizes the current
     28  // window.
     29  await ensureMaximizedWindow(window);
     30 
     31  const sandbox = sinon.createSandbox();
     32  registerCleanupFunction(() => sandbox.restore());
     33 
     34  // The test harness registers test extensions which affects the rendered
     35  // button and panel. This matters especially for tests that want to verify
     36  // the behavior when there are no extensions to render in the list.
     37  // Temporarily fake-hide these extensions to ensure that we start with zero
     38  // extensions from the test's POV.
     39  async function fakeHideExtension(extensionId) {
     40    const { extension } = WebExtensionPolicy.getByID(extensionId);
     41    // This shadows ExtensionData.isHidden of the Extension subclass, causing
     42    // gUnifiedExtensions.getActivePolicies() to ignore the extension.
     43    sandbox.stub(extension, "isHidden").get(() => true);
     44 
     45    const addon = await AddonManager.getAddonByID(extensionId);
     46    sandbox.stub(addon.__AddonInternal__, "hidden").get(() => true);
     47  }
     48  await fakeHideExtension("mochikit@mozilla.org");
     49  await fakeHideExtension("special-powers@mozilla.org");
     50 });
     51 
     52 function getEmptyStateContainer(win) {
     53  let emptyStateBox = win.gUnifiedExtensions.panel.querySelector(
     54    "#unified-extensions-empty-state"
     55  );
     56  ok(emptyStateBox, "Got container for empty panel state");
     57  return emptyStateBox;
     58 }
     59 
     60 function assertIsEmptyPanelOnboardingExtensions(win) {
     61  const emptyStateBox = getEmptyStateContainer(win);
     62  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
     63  is(
     64    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
     65    "unified-extensions-empty-reason-zero-extensions-onboarding",
     66    "Has header when the user does not have any extensions installed"
     67  );
     68  is(
     69    emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
     70    "unified-extensions-empty-content-explain-extensions-onboarding",
     71    "Has description explaining extensions"
     72  );
     73 
     74  const discoverButton = getDiscoverButton(win);
     75  ok(discoverButton, "Got 'Discover button'");
     76  is(
     77    discoverButton.getAttribute("data-l10n-id"),
     78    "unified-extensions-discover-extensions",
     79    "Button in extensions panel should be labeled 'Discover Extensions'"
     80  );
     81  is(
     82    discoverButton.getAttribute("type"),
     83    "primary",
     84    "Discover button should be styled as a primary call-to-action button"
     85  );
     86  const manageExtensionsButton = getListView(win).querySelector(
     87    "#unified-extensions-manage-extensions"
     88  );
     89  ok(
     90    BrowserTestUtils.isHidden(manageExtensionsButton),
     91    "'Manage Extensions' button should be hidden"
     92  );
     93 }
     94 function getDiscoverButton(win) {
     95  return win.gUnifiedExtensions.panel.querySelector(
     96    "#unified-extensions-discover-extensions"
     97  );
     98 }
     99 
    100 async function checkManageExtensionsText(elem) {
    101  const l10nId = elem.dataset.l10nId;
    102  const doc = elem.ownerDocument;
    103  if (doc.hasPendingL10nMutations) {
    104    await BrowserTestUtils.waitForEvent(doc, "L10nMutationsFinished");
    105  }
    106  const expectedButtonText = "Manage extensions";
    107  let expectedTextContent;
    108  if (l10nId === "unified-extensions-empty-content-explain-enable2") {
    109    expectedTextContent =
    110      "Select “Manage extensions” to enable them in settings.";
    111  } else if (l10nId === "unified-extensions-empty-content-explain-manage2") {
    112    expectedTextContent =
    113      "Select “Manage extensions” to manage them in settings.";
    114  } else {
    115    ok(false, `Unexpected data-l10n-id: ${l10nId}`);
    116    return;
    117  }
    118  ok(
    119    expectedTextContent.includes(expectedButtonText),
    120    "Description contains button text ('Manage extensions')"
    121  );
    122  is(expectedTextContent, elem.textContent, "Description has expected text");
    123 }
    124 
    125 add_task(async function test_button_opens_discopane_when_no_extension() {
    126  await BrowserTestUtils.withNewTab(
    127    { gBrowser, url: "about:robots" },
    128    async () => {
    129      const { button } = gUnifiedExtensions;
    130      ok(button, "expected button");
    131 
    132      // This clicks on gUnifiedExtensions.button and waits for panel to show.
    133      await openExtensionsPanel(window);
    134 
    135      assertIsEmptyPanelOnboardingExtensions(window);
    136      const discoverButton = getDiscoverButton(window);
    137 
    138      const tabPromise = BrowserTestUtils.waitForNewTab(
    139        gBrowser,
    140        "about:addons",
    141        true
    142      );
    143 
    144      discoverButton.click();
    145 
    146      const tab = await tabPromise;
    147      is(
    148        gBrowser.currentURI.spec,
    149        "about:addons",
    150        "expected about:addons to be open"
    151      );
    152      is(
    153        gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
    154        "addons://discover/",
    155        "expected about:addons to show the recommendations"
    156      );
    157      BrowserTestUtils.removeTab(tab);
    158    }
    159  );
    160 });
    161 
    162 add_task(async function test_button_opens_extlist_when_all_exts_pinned() {
    163  const extensions = createExtensions([
    164    {
    165      name: "Pinned extension button outside extensions panel",
    166      browser_action: { default_area: "navbar" },
    167    },
    168  ]);
    169  await Promise.all(extensions.map(extension => extension.startup()));
    170 
    171  await SpecialPowers.pushPrefEnv({
    172    set: [
    173      // Set this to another value to make sure not to "accidentally" land on the right page
    174      ["extensions.ui.lastCategory", "addons://list/theme"],
    175      // showPane=true is the default, but to make sure that we get the
    176      // expected behavior for the right reason, explicitly set it to true.
    177      ["extensions.getAddons.showPane", true],
    178    ],
    179  });
    180 
    181  await BrowserTestUtils.withNewTab(
    182    { gBrowser, url: "about:robots" },
    183    async () => {
    184      const { button } = gUnifiedExtensions;
    185      ok(button, "expected button");
    186 
    187      // Primary click should open about:addons.
    188      const tabPromise = BrowserTestUtils.waitForNewTab(
    189        gBrowser,
    190        "about:addons",
    191        true
    192      );
    193 
    194      button.click();
    195 
    196      const tab = await tabPromise;
    197      is(
    198        gBrowser.currentURI.spec,
    199        "about:addons",
    200        "expected about:addons to be open"
    201      );
    202      is(
    203        gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
    204        "addons://list/extension",
    205        "expected about:addons to show the extension list"
    206      );
    207      BrowserTestUtils.removeTab(tab);
    208    }
    209  );
    210 
    211  await SpecialPowers.popPrefEnv();
    212 
    213  await Promise.all(extensions.map(extension => extension.unload()));
    214 });
    215 
    216 add_task(
    217  async function test_button_opens_extlist_when_no_extension_and_pane_disabled() {
    218    // If extensions.getAddons.showPane is set to false, there is no "Recommended" tab,
    219    // so we need to make sure we don't navigate to it.
    220 
    221    await SpecialPowers.pushPrefEnv({
    222      set: [
    223        // Set this to another value to make sure not to "accidentally" land on the right page
    224        ["extensions.ui.lastCategory", "addons://list/theme"],
    225        ["extensions.getAddons.showPane", false],
    226      ],
    227    });
    228 
    229    await BrowserTestUtils.withNewTab(
    230      { gBrowser, url: "about:robots" },
    231      async () => {
    232        // This clicks on gUnifiedExtensions.button and waits for panel to show.
    233        await openExtensionsPanel(window);
    234 
    235        assertIsEmptyPanelOnboardingExtensions(window);
    236        const discoverButton = getDiscoverButton(window);
    237 
    238        const tabPromise = BrowserTestUtils.waitForNewTab(
    239          gBrowser,
    240          "about:addons",
    241          true
    242        );
    243 
    244        discoverButton.click();
    245 
    246        const tab = await tabPromise;
    247        is(
    248          gBrowser.currentURI.spec,
    249          "about:addons",
    250          "expected about:addons to be open"
    251        );
    252        const managerWindow = gBrowser.selectedBrowser.contentWindow;
    253        is(
    254          managerWindow.gViewController.currentViewId,
    255          "addons://list/extension",
    256          "expected about:addons to show the extension list"
    257        );
    258        if (managerWindow.gViewController.isLoading) {
    259          info("Waiting for about:addons to finish loading");
    260          await BrowserTestUtils.waitForEvent(
    261            managerWindow.document,
    262            "view-loaded"
    263          );
    264        }
    265        const amoLink = managerWindow.document.querySelector(
    266          `#empty-addons-message a[data-l10n-name="get-extensions"]`
    267        );
    268        ok(amoLink, "Found link to get extensions");
    269        is(
    270          amoLink.href,
    271          "https://addons.mozilla.org/en-US/firefox/",
    272          "Link points to AMO, where the user can discover extensions"
    273        );
    274        BrowserTestUtils.removeTab(tab);
    275      }
    276    );
    277 
    278    await SpecialPowers.popPrefEnv();
    279  }
    280 );
    281 
    282 add_task(async function test_button_click_in_pbm_without_any_extensions() {
    283  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    284 
    285  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    286  await openExtensionsPanel(win);
    287 
    288  assertIsEmptyPanelOnboardingExtensions(win);
    289  const discoverButton = getDiscoverButton(win);
    290 
    291  // Button click opens about:addons (reuses about:privatebrowsing tab).
    292  // Primary click should open about:addons.
    293  const tabLoadedPromise = BrowserTestUtils.browserStopped(
    294    win.gBrowser.selectedBrowser,
    295    "about:addons"
    296  );
    297 
    298  discoverButton.click();
    299 
    300  await tabLoadedPromise;
    301  is(
    302    win.gBrowser.currentURI.spec,
    303    "about:addons",
    304    "expected about:addons to be open"
    305  );
    306 
    307  // This also closes the new tab.
    308  await BrowserTestUtils.closeWindow(win);
    309 });
    310 
    311 // Tests behavior when the user has extensions installed, but without private
    312 // browsing access. Extensions without private browsing access are not shown,
    313 // and if this was the only extension, then there is no extension to show.
    314 // Instead, a message notifying the user about extensions without private
    315 // access is shown instead.
    316 //
    317 // The scenario of there being an extension with private access is covered by
    318 // test_empty_state_is_hidden_when_panel_is_non_empty below, and by
    319 // test_list_active_extensions_only in browser_unified_extensions.js.
    320 add_task(async function test_button_click_in_pbm_without_private_extensions() {
    321  const extensions = createExtensions([{ name: "Without private access" }]);
    322  await Promise.all(extensions.map(extension => extension.startup()));
    323 
    324  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    325 
    326  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    327  await openExtensionsPanel(win);
    328 
    329  let emptyStateBox = getEmptyStateContainer(win);
    330  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    331  is(
    332    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
    333    "unified-extensions-empty-reason-private-browsing-not-allowed",
    334    "Has header 'You have extensions installed, but not enabled in private windows'"
    335  );
    336  is(
    337    emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    338    "unified-extensions-empty-content-explain-enable2",
    339    "Has description pointing to Manage extensions button."
    340  );
    341 
    342  await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    343 
    344  await BrowserTestUtils.closeWindow(win);
    345 
    346  await Promise.all(extensions.map(extension => extension.unload()));
    347 });
    348 
    349 // In contrast to the above test_button_click_in_pbm_without_private_extensions
    350 // test, this test shows that the empty panel with the private browsing message
    351 // is hidden when there is an extension shown in the private window.
    352 add_task(async function test_empty_state_is_hidden_when_panel_is_non_empty() {
    353  const extensions = [
    354    ...createExtensions([{ name: "Without private access" }]),
    355    ...createExtensions(
    356      [
    357        {
    358          name: "Ext with private browsing access",
    359          browser_specific_settings: { gecko: { id: "@ext-with-pbm-access" } },
    360        },
    361      ],
    362      { incognitoOverride: "spanning" }
    363    ),
    364  ];
    365  await Promise.all(extensions.map(extension => extension.startup()));
    366 
    367  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    368 
    369  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    370  await openExtensionsPanel(win);
    371 
    372  let emptyStateBox = getEmptyStateContainer(win);
    373  ok(BrowserTestUtils.isHidden(emptyStateBox), "Empty state is hidden");
    374 
    375  // Sanity check: the second extension with PBM access is rendered (which is
    376  // the reason that the empty state is hidden).
    377  ok(
    378    getUnifiedExtensionsItem(extensions[1].id, win),
    379    "Found extension with access to PBM in panel in private window"
    380  );
    381 
    382  await BrowserTestUtils.closeWindow(win);
    383 
    384  await Promise.all(extensions.map(extension => extension.unload()));
    385 });
    386 
    387 // Verify empty state when private browsing permission is missing, but
    388 // incognito:not_allowed is specified. See bug 1992179 for context.
    389 add_task(async function test_button_click_in_pbm_and_incognito_not_allowed() {
    390  const extensions = createExtensions([
    391    { name: "ext with incognito:not_allowed", incognito: "not_allowed" },
    392  ]);
    393  await Promise.all(extensions.map(extension => extension.startup()));
    394  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    395 
    396  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    397  await openExtensionsPanel(win);
    398 
    399  let emptyStateBox = getEmptyStateContainer(win);
    400  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    401  is(
    402    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
    403    "unified-extensions-empty-reason-private-browsing-not-allowed",
    404    "Has header 'You have extensions installed, but not enabled in private windows'"
    405  );
    406  is(
    407    emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    408    "unified-extensions-empty-content-explain-manage2",
    409    "Has description pointing to Manage extensions button with text MANAGE, not ENABLE"
    410  );
    411 
    412  await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    413 
    414  await BrowserTestUtils.closeWindow(win);
    415 
    416  await Promise.all(extensions.map(extension => extension.unload()));
    417 });
    418 
    419 // Verify the behavior when there is an extension with private access but is
    420 // pinned, and an extension without private access.
    421 add_task(async function test_button_click_in_pbm_pinned_and_no_access() {
    422  const extensions = [
    423    ...createExtensions([{ name: "Without private access" }]),
    424    ...createExtensions(
    425      [
    426        {
    427          name: "Pinned ext with private browsing access",
    428          browser_action: {
    429            default_area: "navbar", // Pin outside extensions panel.
    430          },
    431          browser_specific_settings: { gecko: { id: "@pin-with-pbm-access" } },
    432        },
    433      ],
    434      { incognitoOverride: "spanning" }
    435    ),
    436  ];
    437  await Promise.all(extensions.map(extension => extension.startup()));
    438  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    439 
    440  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    441  await openExtensionsPanel(win);
    442 
    443  let emptyStateBox = getEmptyStateContainer(win);
    444  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    445  is(
    446    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
    447    "unified-extensions-empty-reason-private-browsing-not-allowed",
    448    "Has header 'You have extensions installed, but not enabled in private windows'"
    449  );
    450  is(
    451    emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    452    "unified-extensions-empty-content-explain-enable2",
    453    "Has description pointing to Manage extensions button."
    454  );
    455 
    456  await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    457 
    458  await BrowserTestUtils.closeWindow(win);
    459 
    460  await Promise.all(extensions.map(extension => extension.unload()));
    461 });
    462 
    463 add_task(async function test_empty_state_with_disabled_addon() {
    464  const [extension] = createExtensions([{ name: "The Only Extension" }]);
    465  await extension.startup();
    466  const addon = await AddonManager.getAddonByID(extension.id);
    467  await addon.disable();
    468 
    469  const win = await BrowserTestUtils.openNewBrowserWindow();
    470 
    471  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    472  await openExtensionsPanel(win);
    473 
    474  let emptyStateBox = getEmptyStateContainer(win);
    475  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    476  is(
    477    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
    478    "unified-extensions-empty-reason-extension-not-enabled",
    479    "Has header 'You have extensions installed, but not enabled'"
    480  );
    481  is(
    482    emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    483    "unified-extensions-empty-content-explain-enable2",
    484    "Has description pointing to Manage extensions button."
    485  );
    486 
    487  await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    488 
    489  await BrowserTestUtils.closeWindow(win);
    490 
    491  await extension.unload();
    492 });
    493 
    494 // This test shows that non-extension add-ons are ignored in evaluating whether
    495 // the empty panel should be shown, even if there is another reason that could
    496 // potentially match for extension types (e.g. add-on being disabled).
    497 add_task(async function test_no_empty_state_with_disabled_non_extension() {
    498  const disabledDictAddon = await promiseInstallWebExtension({
    499    manifest: {
    500      name: "This is a dictionary (definitely not type 'extension') (disabled)",
    501      dictionaries: {},
    502      browser_specific_settings: { gecko: { id: "@dict-disabled" } },
    503    },
    504  });
    505  const dictAddon = await promiseInstallWebExtension({
    506    manifest: {
    507      name: "This is a dictionary (definitely not type 'extension') (enabled)",
    508      dictionaries: {},
    509      browser_specific_settings: { gecko: { id: "@dict-not-disabled" } },
    510    },
    511  });
    512  await disabledDictAddon.disable();
    513  is(disabledDictAddon.isActive, false, "One of the dict add-ons was disabled");
    514 
    515  await BrowserTestUtils.withNewTab(
    516    { gBrowser, url: "about:robots" },
    517    async () => {
    518      // This clicks on gUnifiedExtensions.button and waits for panel to show.
    519      await openExtensionsPanel(window);
    520 
    521      assertIsEmptyPanelOnboardingExtensions(window);
    522      const discoverButton = getDiscoverButton(window);
    523 
    524      const tabPromise = BrowserTestUtils.waitForNewTab(
    525        gBrowser,
    526        "about:addons",
    527        true
    528      );
    529 
    530      discoverButton.click();
    531 
    532      const tab = await tabPromise;
    533      ok(true, "about:addons opened instead of panel about disabled add-ons");
    534      BrowserTestUtils.removeTab(tab);
    535    }
    536  );
    537 
    538  await disabledDictAddon.uninstall();
    539  await dictAddon.uninstall();
    540 });
    541 
    542 // Verifies that if the only add-on is disabled by blocklisting, that we still
    543 // see a panel and that the blocklist message is visible.
    544 // Between hard block and soft blocks, the only difference is that soft block
    545 // can be re-enabled, and that should be reflected in the message.
    546 async function do_test_empty_state_with_blocklisted_addon(isSoftBlock) {
    547  const addonId = "@extension-that-is-blocked";
    548  const addon = await promiseInstallWebExtension({
    549    manifest: {
    550      name: "Name of the blocked ext",
    551      browser_specific_settings: { gecko: { id: addonId } },
    552    },
    553  });
    554 
    555  let promiseBlocklistAttentionUpdated = AddonTestUtils.promiseManagerEvent(
    556    "onBlocklistAttentionUpdated"
    557  );
    558  const cleanupBlocklist = await loadBlocklistRawData({
    559    [isSoftBlock ? "softblocked" : "blocked"]: [addon],
    560  });
    561  info("Wait for onBlocklistAttentionUpdated manager listener call");
    562  await promiseBlocklistAttentionUpdated;
    563 
    564  // This clicks on gUnifiedExtensions.button and waits for panel to show.
    565  await openExtensionsPanel(window);
    566 
    567  // Verify that the blocklist messages appear.
    568  const messages = getMessageBars(window);
    569  is(messages.length, 1, "Expected a message in the Extensions Panel");
    570  Assert.deepEqual(
    571    window.document.l10n.getAttributes(messages[0]),
    572    {
    573      id: isSoftBlock
    574        ? "unified-extensions-mb-blocklist-warning-single2"
    575        : "unified-extensions-mb-blocklist-error-single",
    576      args: {
    577        extensionName: "Name of the blocked ext",
    578        extensionsCount: 1,
    579      },
    580    },
    581    "Blocklist message appears in the (empty) extension panel"
    582  );
    583 
    584  let emptyStateBox = getEmptyStateContainer(window);
    585  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    586  is(
    587    emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
    588    "unified-extensions-empty-reason-extension-not-enabled",
    589    "Has header 'You have extensions installed, but not enabled'"
    590  );
    591  if (isSoftBlock) {
    592    is(
    593      emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    594      "unified-extensions-empty-content-explain-enable2",
    595      "Has description pointing to Manage extensions button with text ENABLE"
    596    );
    597    await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    598  } else {
    599    is(
    600      emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
    601      "unified-extensions-empty-content-explain-manage2",
    602      "Has description pointing to Manage extensions button with text MANAGE, not ENABLE"
    603    );
    604    await checkManageExtensionsText(emptyStateBox.querySelector("description"));
    605  }
    606 
    607  await closeExtensionsPanel(window);
    608 
    609  await cleanupBlocklist();
    610 
    611  // Verify that the messages and empty state gets cleaned up when we re-open
    612  // after unblocking.
    613 
    614  await openExtensionsPanel(window);
    615  is(getMessageBars().length, 0, "No blocklist messages after unblocking");
    616  ok(
    617    BrowserTestUtils.isHidden(getEmptyStateContainer(window)),
    618    "Empty state is hidden when extension is unblocked"
    619  );
    620  await closeExtensionsPanel(window);
    621 
    622  await addon.uninstall();
    623 }
    624 
    625 add_task(async function test_empty_state_with_blocklisted_addon_hardblock() {
    626  await do_test_empty_state_with_blocklisted_addon(/* isSoftBlock */ false);
    627 });
    628 
    629 add_task(async function test_empty_state_with_blocklisted_addon_softblock() {
    630  await do_test_empty_state_with_blocklisted_addon(/* isSoftBlock */ true);
    631 });
    632 
    633 add_task(async function test_safe_mode_notice() {
    634  const sandbox = sinon.createSandbox();
    635  registerCleanupFunction(() => sandbox.restore());
    636 
    637  // Services.appinfo.inSafeMode is ordinarily a constant fixed at browser
    638  // startup. We fake its implementation, and use a separate browser window in
    639  // the test to make sure that any state derived from reading the inSafeMode
    640  // flag is limited to this window.
    641  const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
    642  // Services.appinfo.inSafeMode is a non-configurable property, so to spoof
    643  // its value we stub Services.appinfo and let it fall back to the original
    644  // implementation for every property, except for inSafeMode.
    645  const appinfoStub = new Proxy(Services.appinfo, {
    646    get(target, propertyKey) {
    647      if (propertyKey === "inSafeMode") {
    648        return true;
    649      }
    650      return Reflect.get(target, propertyKey, target);
    651    },
    652  });
    653  sandbox.stub(Services, "appinfo").get(() => appinfoStub);
    654  await openExtensionsPanel(win);
    655 
    656  const messages = getMessageBars(win);
    657  is(messages.length, 1, "Got one message bar");
    658  const bar = messages[0];
    659  is(bar.getAttribute("type"), "info", "Bar is informational notice");
    660  ok(!bar.hasAttribute("dismissable"), "Bar is not dismissable");
    661 
    662  const supportLink = bar.querySelector("a");
    663  is(
    664    supportLink.getAttribute("support-page"),
    665    "diagnose-firefox-issues-using-troubleshoot-mode",
    666    "expected the correct support page ID"
    667  );
    668 
    669  // We don't exactly care which empty state is shown, as the notice is
    670  // independent of the empty state. We just verify as a sanity check that the
    671  // panel is indeed empty, which is most realistic when users enter safe mode.
    672  let emptyStateBox = getEmptyStateContainer(win);
    673  ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
    674 
    675  await closeExtensionsPanel(win);
    676 
    677  // Closing and re-opening should show one bar.
    678  await openExtensionsPanel(win);
    679  is(getMessageBars(win).length, 1, "Still one bar");
    680  await closeExtensionsPanel(win);
    681 
    682  sandbox.restore();
    683  is(Services.appinfo.inSafeMode, false, "Restored original inSafeMode");
    684 
    685  await BrowserTestUtils.closeWindow(win);
    686 });