browser_extension_sideloading.js (15016B)
1 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 2 const { AddonManagerPrivate } = ChromeUtils.importESModule( 3 "resource://gre/modules/AddonManager.sys.mjs" 4 ); 5 6 const { AddonTestUtils } = ChromeUtils.importESModule( 7 "resource://testing-common/AddonTestUtils.sys.mjs" 8 ); 9 10 AddonTestUtils.initMochitest(this); 11 12 AddonTestUtils.hookAMTelemetryEvents(); 13 14 const kSideloaded = true; 15 16 async function createWebExtension(details) { 17 let options = { 18 manifest: { 19 manifest_version: details.manifest_version ?? 2, 20 21 browser_specific_settings: { gecko: { id: details.id } }, 22 23 name: details.name, 24 25 permissions: details.permissions, 26 host_permissions: details.host_permissions, 27 incognito: details.incognito, 28 }, 29 }; 30 31 if (details.iconURL) { 32 options.manifest.icons = { 64: details.iconURL }; 33 } 34 35 let xpi = AddonTestUtils.createTempWebExtensionFile(options); 36 37 await AddonTestUtils.manuallyInstall(xpi); 38 } 39 40 function promiseEvent(eventEmitter, event) { 41 return new Promise(resolve => { 42 eventEmitter.once(event, resolve); 43 }); 44 } 45 46 function getAddonElement(managerWindow, addonId) { 47 return TestUtils.waitForCondition( 48 () => 49 managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`), 50 `Found entry for sideload extension addon "${addonId}" in HTML about:addons` 51 ); 52 } 53 54 function assertSideloadedAddonElementState(addonElement, pressed) { 55 const enableBtn = addonElement.querySelector('[action="toggle-disabled"]'); 56 is( 57 enableBtn.pressed, 58 pressed, 59 `The enable button is ${!pressed ? " not " : ""} pressed` 60 ); 61 is(enableBtn.localName, "moz-toggle", "The enable button is a toggle"); 62 } 63 64 function clickEnableExtension(addonElement) { 65 addonElement.querySelector('[action="toggle-disabled"]').click(); 66 } 67 68 add_task(async function test_sideloading() { 69 const DEFAULT_ICON_URL = 70 "chrome://mozapps/skin/extensions/extensionGeneric.svg"; 71 72 await SpecialPowers.pushPrefEnv({ 73 set: [ 74 ["xpinstall.signatures.required", false], 75 ["extensions.autoDisableScopes", 15], 76 ["extensions.ui.disableUnsignedWarnings", true], 77 ], 78 }); 79 80 Services.fog.testResetFOG(); 81 82 const ID1 = "addon1@tests.mozilla.org"; 83 await createWebExtension({ 84 id: ID1, 85 name: "Test 1", 86 userDisabled: true, 87 permissions: ["history", "https://*/*"], 88 iconURL: "foo-icon.png", 89 }); 90 91 const ID2 = "addon2@tests.mozilla.org"; 92 await createWebExtension({ 93 manifest_version: 3, 94 id: ID2, 95 name: "Test 2", 96 host_permissions: ["<all_urls>"], 97 }); 98 99 const ID3 = "addon3@tests.mozilla.org"; 100 await createWebExtension({ 101 id: ID3, 102 name: "Test 3", 103 permissions: ["<all_urls>"], 104 }); 105 106 const ID4 = "addon4@tests.mozilla.org"; 107 await createWebExtension({ 108 id: ID4, 109 name: "Test 4", 110 incognito: "not_allowed", 111 permissions: [], 112 }); 113 114 const ID5 = "addon5@tests.mozilla.org"; 115 await createWebExtension({ 116 id: ID5, 117 name: "Test 5", 118 incognito: "not_allowed", 119 permissions: ["<all_urls>"], 120 }); 121 122 const ID6 = "addon6@tests.mozilla.org"; 123 await createWebExtension({ 124 id: ID6, 125 name: "Test 6", 126 incognito: "not_allowed", 127 permissions: ["history", "https://*/*"], 128 }); 129 130 testCleanup = async function () { 131 // clear out ExtensionsUI state about sideloaded extensions so 132 // subsequent tests don't get confused. 133 ExtensionsUI.sideloaded.clear(); 134 ExtensionsUI.emit("change"); 135 }; 136 137 // Navigate away from the starting page to force about:addons to load 138 // in a new tab during the tests below. 139 BrowserTestUtils.startLoadingURIString( 140 gBrowser.selectedBrowser, 141 "about:robots" 142 ); 143 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 144 145 registerCleanupFunction(async function () { 146 // Return to about:blank when we're done 147 BrowserTestUtils.startLoadingURIString( 148 gBrowser.selectedBrowser, 149 "about:blank" 150 ); 151 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, { 152 wantLoad: "about:blank", 153 }); 154 }); 155 156 let changePromise = new Promise(resolve => { 157 ExtensionsUI.on("change", function listener() { 158 ExtensionsUI.off("change", listener); 159 resolve(); 160 }); 161 }); 162 ExtensionsUI._checkForSideloaded(); 163 await changePromise; 164 165 // Check for the addons badge on the hamburger menu 166 let menuButton = document.getElementById("PanelUI-menu-button"); 167 is( 168 menuButton.getAttribute("badge-status"), 169 "addon-alert", 170 "Should have addon alert badge" 171 ); 172 173 // Find the menu entries for sideloaded extensions 174 await gCUITestUtils.openMainMenu(); 175 176 let addons = PanelUI.addonNotificationContainer; 177 is( 178 addons.children.length, 179 4, 180 "Have 4 menu entries for sideloaded extensions" 181 ); 182 183 info( 184 "Test disabling sideloaded addon 1 using the permission prompt secondary button" 185 ); 186 187 // Click the first sideloaded extension 188 let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); 189 addons.children[0].click(); 190 191 // The click should hide the main menu. This is currently synchronous. 192 Assert.notEqual( 193 PanelUI.panel.state, 194 "open", 195 "Main menu is closed or closing." 196 ); 197 198 // When we get the permissions prompt, we should be at the extensions 199 // list in about:addons 200 let panel = await popupPromise; 201 is( 202 gBrowser.currentURI.spec, 203 "about:addons", 204 "Foreground tab is at about:addons" 205 ); 206 207 const VIEW = "addons://list/extension"; 208 let win = gBrowser.selectedBrowser.contentWindow; 209 210 await TestUtils.waitForCondition( 211 () => !win.gViewController.isLoading, 212 "about:addons view is fully loaded" 213 ); 214 is( 215 win.gViewController.currentViewId, 216 VIEW, 217 "about:addons is at extensions list" 218 ); 219 220 // Check the contents of the notification, then choose "Cancel" 221 checkNotification( 222 panel, 223 /\/foo-icon\.png$/, 224 [ 225 ["webext-perms-host-description-all-urls"], 226 ["webext-perms-description-history"], 227 ], 228 kSideloaded 229 ); 230 231 panel.secondaryButton.click(); 232 233 let [addon1, addon2, addon3, addon4, addon5, addon6] = 234 await AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4, ID5, ID6]); 235 ok(addon1.seen, "Addon should be marked as seen"); 236 is(addon1.userDisabled, true, "Addon 1 should still be disabled"); 237 is(addon2.userDisabled, true, "Addon 2 should still be disabled"); 238 is(addon3.userDisabled, true, "Addon 3 should still be disabled"); 239 is(addon4.userDisabled, true, "Addon 4 should still be disabled"); 240 is(addon5.userDisabled, true, "Addon 5 should still be disabled"); 241 is(addon6.userDisabled, true, "Addon 6 should still be disabled"); 242 243 BrowserTestUtils.removeTab(gBrowser.selectedTab); 244 245 // Should still have 2 entries in the hamburger menu 246 await gCUITestUtils.openMainMenu(); 247 248 addons = PanelUI.addonNotificationContainer; 249 is( 250 addons.children.length, 251 4, 252 "Have 4 menu entries for sideloaded extensions" 253 ); 254 255 // Close the hamburger menu and go directly to the addons manager 256 await gCUITestUtils.hideMainMenu(); 257 258 win = await BrowserAddonUI.openAddonsMgr(VIEW); 259 await waitAboutAddonsViewLoaded(win.document); 260 261 // about:addons addon entry element. 262 const addonElement = await getAddonElement(win, ID2); 263 264 assertSideloadedAddonElementState(addonElement, false); 265 266 info("Test enabling sideloaded addon 2 from about:addons enable button"); 267 268 // When clicking enable we should see the permissions notification 269 popupPromise = promisePopupNotificationShown("addon-webext-permissions"); 270 clickEnableExtension(addonElement); 271 panel = await popupPromise; 272 checkNotification( 273 panel, 274 DEFAULT_ICON_URL, 275 [["webext-perms-host-description-all-urls"]], 276 kSideloaded 277 ); 278 ok( 279 panel.querySelector(".webext-perm-privatebrowsing moz-checkbox"), 280 "Expect incognito checkbox in sideload prompt" 281 ); 282 283 // Accept the permissions 284 panel.button.click(); 285 await promiseEvent(ExtensionsUI, "change"); 286 287 addon2 = await AddonManager.getAddonByID(ID2); 288 is(addon2.userDisabled, false, "Addon 2 should be enabled"); 289 assertSideloadedAddonElementState(addonElement, true); 290 291 // Should still have 1 entry in the hamburger menu 292 await gCUITestUtils.openMainMenu(); 293 294 addons = PanelUI.addonNotificationContainer; 295 is(addons.children.length, 4, "Have 4 menu entry for sideloaded extensions"); 296 297 // Close the hamburger menu and go to the detail page for this addon 298 await gCUITestUtils.hideMainMenu(); 299 300 win = await BrowserAddonUI.openAddonsMgr( 301 `addons://detail/${encodeURIComponent(ID3)}` 302 ); 303 304 // Trigger all remaining addon install from the app menu, to be able to cover the 305 // post install notification that should be triggered when the permission 306 // dialog is accepted from that flow. 307 const enableSideloadedFromAppMenu = async (addon, testPanelCb) => { 308 popupPromise = promisePopupNotificationShown("addon-webext-permissions"); 309 ExtensionsUI.showSideloaded(gBrowser, addon); 310 panel = await popupPromise; 311 await testPanelCb(); 312 // Accept the permissions 313 panel.button.click(); 314 await promiseEvent(ExtensionsUI, "change"); 315 }; 316 317 info("Test enabling sideloaded addon 3 from app menu"); 318 await enableSideloadedFromAppMenu(addon3, () => { 319 checkNotification( 320 panel, 321 DEFAULT_ICON_URL, 322 [["webext-perms-host-description-all-urls"]], 323 kSideloaded 324 ); 325 }); 326 327 addon3 = await AddonManager.getAddonByID(ID3); 328 is(addon3.userDisabled, false, "Addon 3 should be enabled"); 329 330 addons = PanelUI.addonNotificationContainer; 331 is(addons.children.length, 3, "Have 3 menu entry for sideloaded extensions"); 332 333 info("Test enabling sideloaded addon 4 from app menu"); 334 await enableSideloadedFromAppMenu(addon4, () => { 335 checkNotification(panel, DEFAULT_ICON_URL, [], kSideloaded); 336 }); 337 addon4 = await AddonManager.getAddonByID(ID4); 338 is(addon4.userDisabled, false, "Addon 4 should be enabled"); 339 addons = PanelUI.addonNotificationContainer; 340 is(addons.children.length, 2, "Have 2 menu entry for sideloaded extensions"); 341 342 info("Test enabling sideloaded addon 5 from app menu"); 343 await enableSideloadedFromAppMenu(addon5, () => { 344 checkNotification( 345 panel, 346 DEFAULT_ICON_URL, 347 [["webext-perms-host-description-all-urls"]], 348 kSideloaded 349 ); 350 }); 351 addon5 = await AddonManager.getAddonByID(ID5); 352 is(addon5.userDisabled, false, "Addon 5 should be enabled"); 353 addons = PanelUI.addonNotificationContainer; 354 is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions"); 355 356 info("Test enabling sideloaded addon 6 from app menu"); 357 await enableSideloadedFromAppMenu(addon6, () => { 358 checkNotification( 359 panel, 360 DEFAULT_ICON_URL, 361 [ 362 ["webext-perms-host-description-all-urls"], 363 ["webext-perms-description-history"], 364 ], 365 kSideloaded 366 ); 367 }); 368 addon6 = await AddonManager.getAddonByID(ID6); 369 is(addon6.userDisabled, false, "Addon 6 should be enabled"); 370 371 isnot( 372 menuButton.getAttribute("badge-status"), 373 "addon-alert", 374 "Should no longer have addon alert badge" 375 ); 376 377 await new Promise(resolve => setTimeout(resolve, 100)); 378 379 for (let addon of [addon1, addon2, addon3, addon4, addon5, addon6]) { 380 await addon.uninstall(); 381 } 382 383 isnot( 384 menuButton.getAttribute("badge-status"), 385 "addon-alert", 386 "Should no longer have addon alert badge" 387 ); 388 389 BrowserTestUtils.removeTab(gBrowser.selectedTab); 390 391 // Assert that the expected AddonManager telemetry are being recorded. 392 const expectedExtra = { source: "app-profile", method: "sideload" }; 393 394 const baseEvent = { object: "extension", extra: expectedExtra }; 395 const createBaseEventAddon = n => ({ 396 ...baseEvent, 397 value: `addon${n}@tests.mozilla.org`, 398 }); 399 const getEventsForAddonId = (events, addonId) => 400 events.filter(ev => ev.value === addonId); 401 402 const amEvents = AddonTestUtils.getAMTelemetryEvents(); 403 404 // Test telemetry events for addon1 (1 permission and 1 origin). 405 info("Test telemetry events collected for addon1"); 406 407 const baseEventAddon1 = createBaseEventAddon(1); 408 409 const blocklist_state = `${Ci.nsIBlocklistService.STATE_NOT_BLOCKED}`; 410 411 Assert.deepEqual( 412 AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID1 }), 413 [ 414 { 415 addon_id: ID1, 416 method: "sideload_prompt", 417 addon_type: "extension", 418 source: "app-profile", 419 source_method: "sideload", 420 num_strings: "2", 421 blocklist_state, 422 }, 423 { 424 addon_id: ID1, 425 method: "uninstall", 426 addon_type: "extension", 427 source: "app-profile", 428 source_method: "sideload", 429 blocklist_state, 430 }, 431 ], 432 "Got the expected Glean events for addon1." 433 ); 434 435 const collectedEventsAddon1 = getEventsForAddonId( 436 amEvents, 437 baseEventAddon1.value 438 ); 439 const expectedEventsAddon1 = [ 440 { 441 ...baseEventAddon1, 442 method: "sideload_prompt", 443 extra: { ...expectedExtra, num_strings: "2", blocklist_state }, 444 }, 445 { 446 ...baseEventAddon1, 447 method: "uninstall", 448 extra: { ...expectedExtra, blocklist_state }, 449 }, 450 ]; 451 452 let i = 0; 453 for (let event of collectedEventsAddon1) { 454 Assert.deepEqual( 455 event, 456 expectedEventsAddon1[i++], 457 "Got the expected telemetry event" 458 ); 459 } 460 461 is( 462 collectedEventsAddon1.length, 463 expectedEventsAddon1.length, 464 "Got the expected number of telemetry events for addon1" 465 ); 466 467 const baseEventAddon2 = createBaseEventAddon(2); 468 const collectedEventsAddon2 = getEventsForAddonId( 469 amEvents, 470 baseEventAddon2.value 471 ); 472 const expectedEventsAddon2 = [ 473 { 474 ...baseEventAddon2, 475 method: "sideload_prompt", 476 extra: { ...expectedExtra, num_strings: "1", blocklist_state }, 477 }, 478 { 479 ...baseEventAddon2, 480 method: "enable", 481 extra: { ...expectedExtra, blocklist_state }, 482 }, 483 { 484 ...baseEventAddon2, 485 method: "uninstall", 486 extra: { ...expectedExtra, blocklist_state }, 487 }, 488 ]; 489 490 i = 0; 491 for (let event of collectedEventsAddon2) { 492 Assert.deepEqual( 493 event, 494 expectedEventsAddon2[i++], 495 "Got the expected telemetry event" 496 ); 497 } 498 499 is( 500 collectedEventsAddon2.length, 501 expectedEventsAddon2.length, 502 "Got the expected number of telemetry events for addon2" 503 ); 504 505 Assert.deepEqual( 506 AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID2 }), 507 [ 508 { 509 addon_id: ID2, 510 method: "sideload_prompt", 511 addon_type: "extension", 512 source: "app-profile", 513 source_method: "sideload", 514 num_strings: "1", 515 blocklist_state, 516 }, 517 { 518 addon_id: ID2, 519 method: "enable", 520 addon_type: "extension", 521 source: "app-profile", 522 source_method: "sideload", 523 blocklist_state, 524 }, 525 { 526 addon_id: ID2, 527 method: "uninstall", 528 addon_type: "extension", 529 source: "app-profile", 530 source_method: "sideload", 531 blocklist_state, 532 }, 533 ], 534 "Got the expected Glean events for addon2." 535 ); 536 });