test_nimbus_newtabTrainhopAddon_firstStartup.js (13656B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */ 7 8 /* import-globals-from head_nimbus_trainhop.js */ 9 10 const { AboutHomeStartupCache } = ChromeUtils.importESModule( 11 "resource:///modules/AboutHomeStartupCache.sys.mjs" 12 ); 13 const { sinon } = ChromeUtils.importESModule( 14 "resource://testing-common/Sinon.sys.mjs" 15 ); 16 const { FirstStartup } = ChromeUtils.importESModule( 17 "resource://gre/modules/FirstStartup.sys.mjs" 18 ); 19 const { updateAppInfo } = ChromeUtils.importESModule( 20 "resource://testing-common/AppInfo.sys.mjs" 21 ); 22 23 const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled"; 24 const CATEGORY_NAME = "first-startup-new-profile"; 25 26 add_setup(async () => { 27 Services.fog.testResetFOG(); 28 updateAppInfo(); 29 }); 30 31 /** 32 * Test that AboutNewTabResourceMapping has a first-startup-new-profile 33 * category entry registered for it for the 34 * AboutNewTabResourceMapping.firstStartupNewProfile method. 35 */ 36 add_task(async function test_is_firstStartupNewProfile_registered() { 37 const entry = Services.catMan.getCategoryEntry( 38 CATEGORY_NAME, 39 "resource:///modules/AboutNewTabResourceMapping.sys.mjs" 40 ); 41 Assert.ok( 42 entry, 43 "An entry should exist for resource:///modules/AboutNewTabResourceMapping.sys.mjs" 44 ); 45 Assert.equal( 46 entry, 47 "AboutNewTabResourceMapping.firstStartupNewProfile", 48 "Entry value should point to the `firstStartupNewProfile` method" 49 ); 50 }); 51 52 /** 53 * Test that the firstStartupNewProfile hook gets called during FirstStartup 54 * and performs a restartless install of a train-hop add-on when Nimbus is 55 * configured with one. 56 */ 57 add_task( 58 { skip_if: () => !AppConstants.MOZ_NORMANDY }, 59 async function test_firstStartup_trainhop_restartless_install() { 60 // Enable category tasks for first startup 61 Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); 62 FirstStartup.resetForTesting(); 63 64 // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run 65 mockAboutNewTabUninit(); 66 67 // Sanity check - verify built-in add-on resources have been mapped 68 assertNewTabResourceMapping(); 69 await asyncAssertNewTabAddon({ 70 locationName: BUILTIN_LOCATION_NAME, 71 }); 72 assertTrainhopAddonNimbusExposure({ expectedExposure: false }); 73 74 const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`; 75 76 const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ 77 updateAddonVersion, 78 }); 79 assertTrainhopAddonVersionPref(updateAddonVersion); 80 81 // Track whether firstStartupNewProfile was called 82 let sandbox = sinon.createSandbox(); 83 let firstStartupNewProfileSpy = sandbox.spy( 84 AboutNewTabResourceMapping, 85 "firstStartupNewProfile" 86 ); 87 let aboutHomeStartupClearCacheStub = sandbox.stub( 88 AboutHomeStartupCache, 89 "clearCacheAndUninit" 90 ); 91 92 let submissionPromise = new Promise(resolve => { 93 GleanPings.firstStartup.testBeforeNextSubmit(() => { 94 Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); 95 resolve(); 96 }); 97 }); 98 99 // Run FirstStartup which should trigger our category hook 100 FirstStartup.init(true /* newProfile */); 101 102 await submissionPromise; 103 104 Assert.ok( 105 firstStartupNewProfileSpy.calledOnce, 106 "firstStartupNewProfile should have been called" 107 ); 108 Assert.ok( 109 aboutHomeStartupClearCacheStub.calledOnce, 110 "AboutHomeStartupCache.clearCacheAndUninit called after installing train-hop" 111 ); 112 113 // The train-hop add-on should have been installed restartlessly 114 let addon = await asyncAssertNewTabAddon({ 115 locationName: PROFILE_LOCATION_NAME, 116 version: updateAddonVersion, 117 }); 118 119 Assert.ok(addon, "Train-hop add-on should be installed"); 120 121 // No pending installs should remain since we did a restartless install 122 Assert.deepEqual( 123 await AddonManager.getAllInstalls(), 124 [], 125 "Expect no pending install for restartless install" 126 ); 127 128 sandbox.restore(); 129 130 await nimbusFeatureCleanup(); 131 info( 132 "Simulated browser restart while newtabTrainhopAddon nimbus feature is unenrolled" 133 ); 134 mockAboutNewTabUninit(); 135 await AddonTestUtils.promiseRestartManager(); 136 AboutNewTab.init(); 137 138 // Expected bundled newtab resources mapping for this session. 139 assertNewTabResourceMapping(); 140 await AboutNewTabResourceMapping.updateTrainhopAddonState(); 141 await asyncAssertNewTabAddon({ 142 locationName: BUILTIN_LOCATION_NAME, 143 version: BUILTIN_ADDON_VERSION, 144 }); 145 146 assertTrainhopAddonVersionPref(""); 147 Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); 148 } 149 ); 150 151 /** 152 * Test that if AboutNewTabResourceMapping.init() has already been called 153 * by the time firstStartupNewProfile runs, it logs an error and exits early. 154 * This is not an expected or realistic condition, but we cover it all the same. 155 */ 156 add_task( 157 { skip_if: () => !AppConstants.MOZ_NORMANDY }, 158 async function test_firstStartup_after_initialization() { 159 // Initialize AboutNewTabResourceMapping before FirstStartup runs. 160 AboutNewTabResourceMapping.init(); 161 Assert.ok( 162 AboutNewTabResourceMapping.initialized, 163 "AboutNewTabResourceMapping should be initialized" 164 ); 165 166 Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); 167 FirstStartup.resetForTesting(); 168 169 const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.456`; 170 171 const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ 172 updateAddonVersion, 173 }); 174 175 // Track error logging 176 let errorLogged = false; 177 let sandbox = sinon.createSandbox(); 178 sandbox.stub(AboutNewTabResourceMapping.logger, "error").callsFake(() => { 179 errorLogged = true; 180 }); 181 182 let submissionPromise = new Promise(resolve => { 183 GleanPings.firstStartup.testBeforeNextSubmit(() => { 184 resolve(); 185 }); 186 }); 187 188 FirstStartup.init(true /* newProfile */); 189 await submissionPromise; 190 191 Assert.ok( 192 errorLogged, 193 "An error should have been logged when trying to run after initialization" 194 ); 195 196 // The add-on should NOT have been installed since we were too late 197 await asyncAssertNewTabAddon({ 198 locationName: BUILTIN_LOCATION_NAME, 199 version: BUILTIN_ADDON_VERSION, 200 }); 201 202 sandbox.restore(); 203 await nimbusFeatureCleanup(); 204 Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); 205 } 206 ); 207 208 /** 209 * Test that firstStartupNewProfile doesn't run when the category tasks pref 210 * is disabled. 211 */ 212 add_task( 213 { skip_if: () => !AppConstants.MOZ_NORMANDY }, 214 async function test_firstStartup_category_disabled() { 215 // Disable category tasks 216 Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, false); 217 FirstStartup.resetForTesting(); 218 219 // Reset AboutNewTabResourceMapping state 220 mockAboutNewTabUninit(); 221 222 const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.789`; 223 224 const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ 225 updateAddonVersion, 226 }); 227 228 let sandbox = sinon.createSandbox(); 229 let firstStartupNewProfileSpy = sandbox.spy( 230 AboutNewTabResourceMapping, 231 "firstStartupNewProfile" 232 ); 233 234 let submissionPromise = new Promise(resolve => { 235 GleanPings.firstStartup.testBeforeNextSubmit(() => { 236 resolve(); 237 }); 238 }); 239 240 FirstStartup.init(true /* newProfile */); 241 await submissionPromise; 242 243 Assert.ok( 244 !firstStartupNewProfileSpy.called, 245 "firstStartupNewProfile should not have been called when pref is disabled" 246 ); 247 248 // The add-on should still be the builtin version 249 await asyncAssertNewTabAddon({ 250 locationName: BUILTIN_LOCATION_NAME, 251 version: BUILTIN_ADDON_VERSION, 252 }); 253 254 sandbox.restore(); 255 await nimbusFeatureCleanup(); 256 Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); 257 } 258 ); 259 260 /** 261 * Test that if AboutNewTabResourceMapping.init() is called after the XPI 262 * download has started but before onInstallPostponed is called, we skip 263 * attempting to force the restartless install and fall back to a staged 264 * install instead. 265 */ 266 add_task( 267 { skip_if: () => !AppConstants.MOZ_NORMANDY }, 268 async function test_firstStartup_init_during_download() { 269 Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); 270 FirstStartup.resetForTesting(); 271 272 // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run 273 mockAboutNewTabUninit(); 274 275 assertNewTabResourceMapping(); 276 await asyncAssertNewTabAddon({ 277 locationName: BUILTIN_LOCATION_NAME, 278 }); 279 280 const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.999`; 281 282 const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ 283 updateAddonVersion, 284 }); 285 assertTrainhopAddonVersionPref(updateAddonVersion); 286 287 // Stub updateTrainhopAddonState to call init() in the middle of its execution 288 let sandbox = sinon.createSandbox(); 289 let aboutNewTabInitSpy = sandbox.spy(AboutNewTabResourceMapping, "init"); 290 291 let originalUpdateTrainhopAddonState = 292 AboutNewTabResourceMapping.updateTrainhopAddonState.bind( 293 AboutNewTabResourceMapping 294 ); 295 296 let updateTrainhopStarted = false; 297 sandbox 298 .stub(AboutNewTabResourceMapping, "updateTrainhopAddonState") 299 .callsFake(async function (forceRestartlessInstall) { 300 updateTrainhopStarted = true; 301 302 // Start the update process 303 let updatePromise = originalUpdateTrainhopAddonState( 304 forceRestartlessInstall 305 ); 306 307 // Call init immediately after starting the update, simulating 308 // the browser window opening during the XPI download 309 info( 310 "Calling AboutNewTabResourceMapping.init() during updateTrainhopAddonState" 311 ); 312 AboutNewTabResourceMapping.init(); 313 314 // Wait for the update to complete 315 await updatePromise; 316 }); 317 318 let submissionPromise = new Promise(resolve => { 319 GleanPings.firstStartup.testBeforeNextSubmit(() => { 320 Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); 321 resolve(); 322 }); 323 }); 324 325 FirstStartup.init(true /* newProfile */); 326 await submissionPromise; 327 328 Assert.ok( 329 updateTrainhopStarted, 330 "updateTrainhopAddonState should have started" 331 ); 332 Assert.ok( 333 aboutNewTabInitSpy.calledOnce, 334 "AboutNewTabResourceMapping.init should have been called" 335 ); 336 337 // The add-on should be staged for install, not installed restartlessly 338 await asyncAssertNewTabAddon({ 339 locationName: BUILTIN_LOCATION_NAME, 340 version: BUILTIN_ADDON_VERSION, 341 }); 342 343 // Verify there's a pending install 344 const pendingInstall = (await AddonManager.getAllInstalls()).find( 345 install => install.addon.id === BUILTIN_ADDON_ID 346 ); 347 Assert.ok(pendingInstall, "Should have a pending install"); 348 Assert.equal( 349 pendingInstall.state, 350 AddonManager.STATE_POSTPONED, 351 "Install should be postponed" 352 ); 353 Assert.equal( 354 pendingInstall.addon.version, 355 updateAddonVersion, 356 "Pending install should be for the train-hop version" 357 ); 358 359 // Clean up 360 await cancelPendingInstall(pendingInstall); 361 sandbox.restore(); 362 await nimbusFeatureCleanup(); 363 assertTrainhopAddonVersionPref(""); 364 Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); 365 } 366 ); 367 368 /** 369 * Test that the TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID Nimbus feature can be 370 * used to remotely disable the FirstStartup force-install flow. 371 */ 372 add_task( 373 { skip_if: () => !AppConstants.MOZ_NORMANDY }, 374 async function test_firstStartup_remote_disable() { 375 // Enable category tasks for first startup 376 Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); 377 FirstStartup.resetForTesting(); 378 379 // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run 380 mockAboutNewTabUninit(); 381 382 // Sanity check - verify built-in add-on resources have been mapped 383 assertNewTabResourceMapping(); 384 await asyncAssertNewTabAddon({ 385 locationName: BUILTIN_LOCATION_NAME, 386 }); 387 assertTrainhopAddonNimbusExposure({ expectedExposure: false }); 388 389 const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`; 390 391 const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ 392 updateAddonVersion, 393 }); 394 assertTrainhopAddonVersionPref(updateAddonVersion); 395 396 const firstStartupFeatureCleanup = 397 await NimbusTestUtils.enrollWithFeatureConfig( 398 { 399 featureId: TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID, 400 value: { enabled: false }, 401 }, 402 { isRollout: true } 403 ); 404 405 // Track whether firstStartupNewProfile was called 406 let sandbox = sinon.createSandbox(); 407 let firstStartupNewProfileSpy = sandbox.spy( 408 AboutNewTabResourceMapping, 409 "firstStartupNewProfile" 410 ); 411 412 let submissionPromise = new Promise(resolve => { 413 GleanPings.firstStartup.testBeforeNextSubmit(() => { 414 Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); 415 resolve(); 416 }); 417 }); 418 419 // Run FirstStartup which should trigger our category hook 420 FirstStartup.init(true /* newProfile */); 421 422 await submissionPromise; 423 424 Assert.ok( 425 firstStartupNewProfileSpy.calledOnce, 426 "firstStartupNewProfile should have been called" 427 ); 428 429 // The add-on should still be the builtin version 430 await asyncAssertNewTabAddon({ 431 locationName: BUILTIN_LOCATION_NAME, 432 version: BUILTIN_ADDON_VERSION, 433 }); 434 435 sandbox.restore(); 436 await nimbusFeatureCleanup(); 437 await firstStartupFeatureCleanup(); 438 assertTrainhopAddonVersionPref(""); 439 Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); 440 } 441 );