browser_extension_update_background.js (9484B)
1 const { AddonManagerPrivate } = ChromeUtils.importESModule( 2 "resource://gre/modules/AddonManager.sys.mjs" 3 ); 4 5 const { AddonTestUtils } = ChromeUtils.importESModule( 6 "resource://testing-common/AddonTestUtils.sys.mjs" 7 ); 8 9 AddonTestUtils.initMochitest(this); 10 AddonTestUtils.hookAMTelemetryEvents(); 11 12 const ID = "update2@tests.mozilla.org"; 13 const ID_ICON = "update_icon2@tests.mozilla.org"; 14 const ID_PERMS = "update_perms@tests.mozilla.org"; 15 const ID_LEGACY = "legacy_update@tests.mozilla.org"; 16 const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source"; 17 18 requestLongerTimeout(2); 19 20 function promiseViewLoaded(tab, viewid) { 21 let win = tab.linkedBrowser.contentWindow; 22 if ( 23 win.gViewController && 24 !win.gViewController.isLoading && 25 win.gViewController.currentViewId == viewid 26 ) { 27 return Promise.resolve(); 28 } 29 30 return waitAboutAddonsViewLoaded(win.document); 31 } 32 33 function getBadgeStatus() { 34 let menuButton = document.getElementById("PanelUI-menu-button"); 35 return menuButton.getAttribute("badge-status"); 36 } 37 38 // Set some prefs that apply to all the tests in this file 39 add_setup(async function () { 40 await SpecialPowers.pushPrefEnv({ 41 set: [ 42 // We don't have pre-pinned certificates for the local mochitest server 43 ["extensions.install.requireBuiltInCerts", false], 44 ["extensions.update.requireBuiltInCerts", false], 45 ], 46 }); 47 48 // Navigate away from the initial page so that about:addons always 49 // opens in a new tab during tests 50 BrowserTestUtils.startLoadingURIString( 51 gBrowser.selectedBrowser, 52 "about:robots" 53 ); 54 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 55 56 registerCleanupFunction(async function () { 57 // Return to about:blank when we're done 58 BrowserTestUtils.startLoadingURIString( 59 gBrowser.selectedBrowser, 60 "about:blank" 61 ); 62 await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, { 63 wantLoad: "about:blank", 64 }); 65 }); 66 }); 67 68 // Helper function to test background updates. 69 async function backgroundUpdateTest(url, id, checkIconFn) { 70 await SpecialPowers.pushPrefEnv({ 71 set: [ 72 // Turn on background updates 73 ["extensions.update.enabled", true], 74 75 // Point updates to the local mochitest server 76 [ 77 "extensions.update.background.url", 78 `${BASE}/browser_webext_update.json`, 79 ], 80 ], 81 }); 82 83 Services.fog.testResetFOG(); 84 85 // Install version 1.0 of the test extension 86 let addon = await promiseInstallAddon(url, { 87 source: FAKE_INSTALL_TELEMETRY_SOURCE, 88 }); 89 let addonId = addon.id; 90 91 ok(addon, "Addon was installed"); 92 is(getBadgeStatus(), null, "Should not start out with an addon alert badge"); 93 94 // Trigger an update check and wait for the update for this addon 95 // to be downloaded. 96 let updatePromise = promiseInstallEvent(addon, "onDownloadEnded"); 97 98 AddonManagerPrivate.backgroundUpdateCheck(); 99 await updatePromise; 100 101 is(getBadgeStatus(), "addon-alert", "Should have addon alert badge"); 102 103 // Find the menu entry for the update 104 await gCUITestUtils.openMainMenu(); 105 106 let addons = PanelUI.addonNotificationContainer; 107 is(addons.children.length, 1, "Have a menu entry for the update"); 108 109 // Click the menu item 110 let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons"); 111 let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); 112 addons.children[0].click(); 113 114 // The click should hide the main menu. This is currently synchronous. 115 Assert.notEqual( 116 PanelUI.panel.state, 117 "open", 118 "Main menu is closed or closing." 119 ); 120 121 // about:addons should load and go to the list of extensions 122 let tab = await tabPromise; 123 is( 124 tab.linkedBrowser.currentURI.spec, 125 "about:addons", 126 "Browser is at about:addons" 127 ); 128 129 const VIEW = "addons://list/extension"; 130 await promiseViewLoaded(tab, VIEW); 131 let win = tab.linkedBrowser.contentWindow; 132 ok(!win.gViewController.isLoading, "about:addons view is fully loaded"); 133 is( 134 win.gViewController.currentViewId, 135 VIEW, 136 "about:addons is at extensions list" 137 ); 138 139 // Wait for the permission prompt, check the contents 140 let panel = await popupPromise; 141 checkIconFn(panel.getAttribute("icon")); 142 143 // The original extension has 1 promptable permission and the new one 144 // has 2 (history and <all_urls>) plus 1 non-promptable permission (cookies). 145 // So we should only see the 1 new promptable permission in the notification. 146 let permissionsListEl = document.getElementById( 147 "addon-webext-perm-list-required" 148 ); 149 ok(!permissionsListEl.hidden, "Permissions list is visible"); 150 ok(permissionsListEl.textContent, "Permissions list contains text"); 151 is( 152 permissionsListEl.childElementCount, 153 1, 154 "Expect only 1 permission entry in the Permissions list" 155 ); 156 157 // Cancel the update. 158 panel.secondaryButton.click(); 159 160 addon = await AddonManager.getAddonByID(id); 161 is(addon.version, "1.0", "Should still be running the old version"); 162 163 BrowserTestUtils.removeTab(tab); 164 165 // Alert badge and hamburger menu items should be gone 166 is(getBadgeStatus(), null, "Addon alert badge should be gone"); 167 168 await gCUITestUtils.openMainMenu(); 169 addons = PanelUI.addonNotificationContainer; 170 is(addons.children.length, 0, "Update menu entries should be gone"); 171 await gCUITestUtils.hideMainMenu(); 172 173 // Re-check for an update 174 updatePromise = promiseInstallEvent(addon, "onDownloadEnded"); 175 await AddonManagerPrivate.backgroundUpdateCheck(); 176 await updatePromise; 177 178 is(getBadgeStatus(), "addon-alert", "Should have addon alert badge"); 179 180 // Find the menu entry for the update 181 await gCUITestUtils.openMainMenu(); 182 183 addons = PanelUI.addonNotificationContainer; 184 is(addons.children.length, 1, "Have a menu entry for the update"); 185 186 // Click the menu item 187 tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons", true); 188 popupPromise = promisePopupNotificationShown("addon-webext-permissions"); 189 190 addons.children[0].click(); 191 192 // Wait for about:addons to load 193 tab = await tabPromise; 194 is(tab.linkedBrowser.currentURI.spec, "about:addons"); 195 196 await promiseViewLoaded(tab, VIEW); 197 win = tab.linkedBrowser.contentWindow; 198 ok(!win.gViewController.isLoading, "about:addons view is fully loaded"); 199 is( 200 win.gViewController.currentViewId, 201 VIEW, 202 "about:addons is at extensions list" 203 ); 204 205 // Wait for the permission prompt and accept it this time 206 updatePromise = waitForUpdate(addon); 207 panel = await popupPromise; 208 panel.button.click(); 209 210 addon = await updatePromise; 211 is(addon.version, "2.0", "Should have upgraded to the new version"); 212 213 BrowserTestUtils.removeTab(tab); 214 215 is(getBadgeStatus(), null, "Addon alert badge should be gone"); 216 217 await addon.uninstall(); 218 await SpecialPowers.popPrefEnv(); 219 220 let gleanUpdates = AddonTestUtils.getAMGleanEvents("update"); 221 222 // Test that the expected telemetry events have been recorded (and that they include the 223 // permission_prompt event). 224 const amEvents = AddonTestUtils.getAMTelemetryEvents(); 225 const updateEvents = amEvents 226 .filter(evt => evt.method === "update") 227 .map(evt => { 228 delete evt.value; 229 return evt; 230 }); 231 232 const expectedSteps = [ 233 // First update (cancelled). 234 "started", 235 "download_started", 236 "download_completed", 237 "permissions_prompt", 238 "cancelled", 239 // Second update (completed). 240 "started", 241 "download_started", 242 "download_completed", 243 "permissions_prompt", 244 "completed", 245 ]; 246 247 Assert.deepEqual( 248 expectedSteps, 249 updateEvents.map(evt => evt.extra && evt.extra.step), 250 "Got the steps from the collected telemetry events" 251 ); 252 253 Assert.deepEqual( 254 expectedSteps, 255 gleanUpdates.map(evt => evt.step), 256 "Got the steps from the collected Glean events." 257 ); 258 259 const method = "update"; 260 const object = "extension"; 261 const baseExtra = { 262 addon_id: addonId, 263 source: FAKE_INSTALL_TELEMETRY_SOURCE, 264 step: "permissions_prompt", 265 updated_from: "app", 266 }; 267 268 // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going 269 // to be listed in the permission prompt. 270 Assert.deepEqual( 271 updateEvents.filter( 272 evt => evt.extra && evt.extra.step === "permissions_prompt" 273 ), 274 [ 275 { method, object, extra: { ...baseExtra, num_strings: "1" } }, 276 { method, object, extra: { ...baseExtra, num_strings: "1" } }, 277 ], 278 "Got the expected permission_prompts events" 279 ); 280 281 Assert.deepEqual( 282 gleanUpdates.filter(e => e.step === "permissions_prompt"), 283 [ 284 { ...baseExtra, addon_type: object, num_strings: "1" }, 285 { ...baseExtra, addon_type: object, num_strings: "1" }, 286 ], 287 "Got the expected permission_prompt events from Glean." 288 ); 289 } 290 291 function checkDefaultIcon(icon) { 292 is( 293 icon, 294 "chrome://mozapps/skin/extensions/extensionGeneric.svg", 295 "Popup has the default extension icon" 296 ); 297 } 298 299 add_task(() => 300 backgroundUpdateTest( 301 `${BASE}/browser_webext_update1.xpi`, 302 ID, 303 checkDefaultIcon 304 ) 305 ); 306 function checkNonDefaultIcon(icon) { 307 // The icon should come from the extension, don't bother with the precise 308 // path, just make sure we've got a jar url pointing to the right path 309 // inside the jar. 310 ok(icon.startsWith("jar:file://"), "Icon is a jar url"); 311 ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar"); 312 } 313 314 add_task(() => 315 backgroundUpdateTest( 316 `${BASE}/browser_webext_update_icon1.xpi`, 317 ID_ICON, 318 checkNonDefaultIcon 319 ) 320 );