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 });