tor-browser

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

commit 36a04696deb9fb880e2d0a8f6b53a27368336d15
parent 1dc0dfe400cfa1fc8c650283404e1499bdba475a
Author: Mike Conley <mconley@mozilla.com>
Date:   Wed, 17 Dec 2025 16:19:03 +0000

Bug 2001387 - Part 3: Have AboutNewTabResourceMapping register a first-startup blocker for new profiles to install any potential train-hops. r=rpl

Differential Revision: https://phabricator.services.mozilla.com/D276296

Diffstat:
Mbrowser/components/BrowserComponents.manifest | 2++
Mbrowser/components/newtab/AboutNewTabResourceMapping.sys.mjs | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mbrowser/components/newtab/test/xpcshell/head_nimbus_trainhop.js | 1+
Abrowser/components/newtab/test/xpcshell/test_nimbus_newtabTrainhopAddon_firstStartup.js | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/newtab/test/xpcshell/xpcshell.toml | 3+++
Mtoolkit/components/nimbus/FeatureManifest.yaml | 14++++++++++++++
6 files changed, 559 insertions(+), 24 deletions(-)

diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest @@ -9,6 +9,8 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} +category first-startup-new-profile resource:///modules/AboutNewTabResourceMapping.sys.mjs AboutNewTabResourceMapping.firstStartupNewProfile + # Browser global components initializing before UI startup category browser-before-ui-startup resource:///modules/sessionstore/SessionStore.sys.mjs SessionStore.init category browser-before-ui-startup resource:///modules/BuiltInThemes.sys.mjs BuiltInThemes.maybeInstallActiveBuiltInTheme diff --git a/browser/components/newtab/AboutNewTabResourceMapping.sys.mjs b/browser/components/newtab/AboutNewTabResourceMapping.sys.mjs @@ -9,6 +9,8 @@ export const BUILTIN_ADDON_ID = "newtab@mozilla.org"; export const DISABLE_NEWTAB_AS_ADDON_PREF = "browser.newtabpage.disableNewTabAsAddon"; export const TRAINHOP_NIMBUS_FEATURE_ID = "newtabTrainhopAddon"; +export const TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID = + "newtabTrainhopFirstStartup"; export const TRAINHOP_XPI_BASE_URL_PREF = "browser.newtabpage.trainhopAddon.xpiBaseURL"; export const TRAINHOP_XPI_VERSION_PREF = @@ -28,6 +30,7 @@ const lazy = XPCOMUtils.declareLazy({ AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs", AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", NewTabGleanUtils: "resource://newtab/lib/NewTabGleanUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", @@ -109,16 +112,7 @@ export var AboutNewTabResourceMapping = { return; } - this.logger = console.createInstance({ - prefix: "AboutNewTabResourceMapping", - maxLogLevel: Services.prefs.getBoolPref( - "browser.newtabpage.resource-mapping.log", - false - ) - ? "Debug" - : "Warn", - }); - this.logger.debug("Initializing"); + this.logger.debug("Initializing:"); // NOTE: this pref is read only once per session on purpose // (and it is expected to be used by the resource mapping logic @@ -341,13 +335,13 @@ export var AboutNewTabResourceMapping = { registry.updateSources([newtabFileSource]); this.logger.debug( "Newtab strings updated for ", - availableSupportedLocales + Array.from(availableSupportedLocales) ); } else { registry.registerSources([newtabFileSource]); this.logger.debug( "Newtab strings registered for ", - availableSupportedLocales + Array.from(availableSupportedLocales) ); } }, @@ -428,7 +422,7 @@ export var AboutNewTabResourceMapping = { * installed or pending to be installed). Rejects on failures or unexpected cancellations * during installation or uninstallation process. */ - async updateTrainhopAddonState() { + async updateTrainhopAddonState(forceRestartlessInstall = false) { if (this.inSafeMode) { this.logger.debug( "train-hop add-on update state disabled while running in SafeMode" @@ -442,6 +436,10 @@ export var AboutNewTabResourceMapping = { defaultValues: { addon_version: null, xpi_download_path: null }, }); + this.logger.debug("Force restartless install: ", forceRestartlessInstall); + this.logger.debug("Received addon version:", addon_version); + this.logger.debug("Received XPI download path:", xpi_download_path); + let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID); // Uninstall train-hop add-on xpi if its resources are not currently @@ -535,6 +533,7 @@ export var AboutNewTabResourceMapping = { await this._installTrainhopAddon({ trainhopAddonVersion: addon_version, xpiDownloadURL, + forceRestartlessInstall, }); }, @@ -545,12 +544,21 @@ export var AboutNewTabResourceMapping = { * @param {object} params * @param {string} params.trainhopAddonVersion - The version of the train-hop add-on to install. * @param {string} params.xpiDownloadURL - The URL from which to download the XPI file. + * @param {boolean} params.forceRestartlessInstall + * After the XPI is downloaded, attempt to complete a restartless install. Note that if + * AboutNewTabResourceMapping.init has been called by the time the XPI has finished + * downloading, this directive is ignored, and we fallback to installing on the next + * restart. * * @returns {Promise<void>} * Resolves when the train-hop add-on installation is completed or not needed, or rejects * on failures or unexpected cancellations hit during the installation process. */ - async _installTrainhopAddon({ trainhopAddonVersion, xpiDownloadURL }) { + async _installTrainhopAddon({ + trainhopAddonVersion, + xpiDownloadURL, + forceRestartlessInstall, + }) { if ( this._builtinVersion && Services.vc.compare(this._builtinVersion, trainhopAddonVersion) >= 0 @@ -603,7 +611,7 @@ export var AboutNewTabResourceMapping = { ); const deferred = Promise.withResolvers(); newInstall.addListener({ - onDownloadEnded() { + onDownloadEnded: () => { if ( newInstall.addon.id !== BUILTIN_ADDON_ID || newInstall.addon.version !== trainhopAddonVersion @@ -630,30 +638,51 @@ export var AboutNewTabResourceMapping = { ); newInstall.cancel(); } + + this.logger.debug("Train-hop download ended"); + }, + onInstallPostponed: () => { + this.logger.debug("Train-hop install postponed, as expected"); + if (forceRestartlessInstall && !this.initialized) { + this.logger.debug("Forcing restartless install of train-hop"); + newInstall.continuePostponedInstall(); + } else { + this.logger.debug("Not forcing restartless install"); + if (forceRestartlessInstall) { + this.logger.debug( + "We must have initialized before the XPI finished downloading." + ); + } + deferred.resolve(); + } }, - onInstallPostponed() { - deferred.resolve(); + onInstallEnded: () => { + this.logger.debug("Train-hop restartless install ended"); + if (forceRestartlessInstall) { + this.logger.debug("Resolving train-hop install promise"); + deferred.resolve(); + } }, - onDownloadCancelled() { + onDownloadCancelled: () => { deferred.reject( new Error( `Unexpected download cancelled while downloading xpi from ${xpiDownloadURL}` ) ); }, - onDownloadFailed() { + onDownloadFailed: () => { deferred.reject( new Error(`Failed to download xpi from ${xpiDownloadURL}`) ); }, - onInstallCancelled() { + onInstallCancelled: () => { deferred.reject( new Error( `Unexpected install cancelled while installing xpi from ${xpiDownloadURL}` ) ); }, - onInstallFailed() { + onInstallFailed: () => { deferred.reject( new Error(`Failed to install xpi from ${xpiDownloadURL}`) ); @@ -661,9 +690,16 @@ export var AboutNewTabResourceMapping = { }); newInstall.install(); await deferred.promise; - this.logger.debug( - `train-hop add-on ${trainhopAddonVersion} downloaded and pending install on next startup` - ); + + if (forceRestartlessInstall) { + this.logger.debug( + `train-hop add-on ${trainhopAddonVersion} downloaded and we will attempt a restartless install` + ); + } else { + this.logger.debug( + `train-hop add-on ${trainhopAddonVersion} downloaded and pending install on next startup` + ); + } } catch (e) { this.logger.error(`train-hop add-on install failure: ${e}`); } @@ -694,4 +730,54 @@ export var AboutNewTabResourceMapping = { } return false; }, + + /** + * This is registered to be called on first startup for new profiles on + * Windows. It is expected to be called very early on in the lifetime of + * new profiles, such that the AboutNewTabResourceMapping.init routine has + * not yet had a chance to run. + * + * @returns {Promise<void>} + */ + async firstStartupNewProfile() { + if (this.initialized) { + this.logger.error( + "firstStartupNewProfile is being run after AboutNewTabResourceMapping initializes, so we're too late." + ); + return; + } + this.logger.debug( + "First startup with a new profile. Checking for any train-hops to perform restartless install." + ); + await lazy.ExperimentAPI.ready(); + + const nimbusFeature = + lazy.NimbusFeatures[TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID]; + await nimbusFeature.ready(); + const { enabled } = nimbusFeature.getAllVariables({ + defaultValues: { enabled: true }, + }); + if (!enabled) { + // We've been configured to bypass the FirstStartup install for + // train-hops, so exit now. + this.logger.debug( + "Not forcing install of any newtab XPIs, as we're currently configured not to." + ); + return; + } + + await lazy.AddonManager.readyPromise; + await this.updateTrainhopAddonState(true /* forceRestartlessInstall */); + this.logger.debug("First startup - new profile done"); + }, }; + +AboutNewTabResourceMapping.logger = console.createInstance({ + prefix: "AboutNewTabResourceMapping", + maxLogLevel: Services.prefs.getBoolPref( + "browser.newtabpage.resource-mapping.log", + false + ) + ? "Debug" + : "Warn", +}); diff --git a/browser/components/newtab/test/xpcshell/head_nimbus_trainhop.js b/browser/components/newtab/test/xpcshell/head_nimbus_trainhop.js @@ -25,6 +25,7 @@ const { AboutNewTabResourceMapping, BUILTIN_ADDON_ID, TRAINHOP_NIMBUS_FEATURE_ID, + TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID, TRAINHOP_XPI_BASE_URL_PREF, TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF, TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF, diff --git a/browser/components/newtab/test/xpcshell/test_nimbus_newtabTrainhopAddon_firstStartup.js b/browser/components/newtab/test/xpcshell/test_nimbus_newtabTrainhopAddon_firstStartup.js @@ -0,0 +1,429 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */ + +/* import-globals-from head_nimbus_trainhop.js */ + +const { AboutHomeStartupCache } = ChromeUtils.importESModule( + "resource:///modules/AboutHomeStartupCache.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { FirstStartup } = ChromeUtils.importESModule( + "resource://gre/modules/FirstStartup.sys.mjs" +); +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const PREF_CATEGORY_TASKS = "first-startup.category-tasks-enabled"; +const CATEGORY_NAME = "first-startup-new-profile"; + +add_setup(async () => { + Services.fog.testResetFOG(); + updateAppInfo(); +}); + +/** + * Test that AboutNewTabResourceMapping has a first-startup-new-profile + * category entry registered for it for the + * AboutNewTabResourceMapping.firstStartupNewProfile method. + */ +add_task(async function test_is_firstStartupNewProfile_registered() { + const entry = Services.catMan.getCategoryEntry( + CATEGORY_NAME, + "resource:///modules/AboutNewTabResourceMapping.sys.mjs" + ); + Assert.ok( + entry, + "An entry should exist for resource:///modules/AboutNewTabResourceMapping.sys.mjs" + ); + Assert.equal( + entry, + "AboutNewTabResourceMapping.firstStartupNewProfile", + "Entry value should point to the `firstStartupNewProfile` method" + ); +}); + +/** + * Test that the firstStartupNewProfile hook gets called during FirstStartup + * and performs a restartless install of a train-hop add-on when Nimbus is + * configured with one. + */ +add_task( + { skip_if: () => !AppConstants.MOZ_NORMANDY }, + async function test_firstStartup_trainhop_restartless_install() { + // Enable category tasks for first startup + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); + FirstStartup.resetForTesting(); + + // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run + mockAboutNewTabUninit(); + + // Sanity check - verify built-in add-on resources have been mapped + assertNewTabResourceMapping(); + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + }); + assertTrainhopAddonNimbusExposure({ expectedExposure: false }); + + const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`; + + const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ + updateAddonVersion, + }); + assertTrainhopAddonVersionPref(updateAddonVersion); + + // Track whether firstStartupNewProfile was called + let sandbox = sinon.createSandbox(); + let firstStartupNewProfileSpy = sandbox.spy( + AboutNewTabResourceMapping, + "firstStartupNewProfile" + ); + let aboutHomeStartupClearCacheStub = sandbox.stub( + AboutHomeStartupCache, + "clearCacheAndUninit" + ); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); + resolve(); + }); + }); + + // Run FirstStartup which should trigger our category hook + FirstStartup.init(true /* newProfile */); + + await submissionPromise; + + Assert.ok( + firstStartupNewProfileSpy.calledOnce, + "firstStartupNewProfile should have been called" + ); + Assert.ok( + aboutHomeStartupClearCacheStub.calledOnce, + "AboutHomeStartupCache.clearCacheAndUninit called after installing train-hop" + ); + + // The train-hop add-on should have been installed restartlessly + let addon = await asyncAssertNewTabAddon({ + locationName: PROFILE_LOCATION_NAME, + version: updateAddonVersion, + }); + + Assert.ok(addon, "Train-hop add-on should be installed"); + + // No pending installs should remain since we did a restartless install + Assert.deepEqual( + await AddonManager.getAllInstalls(), + [], + "Expect no pending install for restartless install" + ); + + sandbox.restore(); + + // Uninstall the force-installed train-hop add-on before cleanup + await addon.uninstall(); + + await nimbusFeatureCleanup(); + assertTrainhopAddonVersionPref(""); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); + } +); + +/** + * Test that if AboutNewTabResourceMapping.init() has already been called + * by the time firstStartupNewProfile runs, it logs an error and exits early. + * This is not an expected or realistic condition, but we cover it all the same. + */ +add_task( + { skip_if: () => !AppConstants.MOZ_NORMANDY }, + async function test_firstStartup_after_initialization() { + // Initialize AboutNewTabResourceMapping before FirstStartup runs. + AboutNewTabResourceMapping.init(); + Assert.ok( + AboutNewTabResourceMapping.initialized, + "AboutNewTabResourceMapping should be initialized" + ); + + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); + FirstStartup.resetForTesting(); + + const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.456`; + + const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ + updateAddonVersion, + }); + + // Track error logging + let errorLogged = false; + let sandbox = sinon.createSandbox(); + sandbox.stub(AboutNewTabResourceMapping.logger, "error").callsFake(() => { + errorLogged = true; + }); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + resolve(); + }); + }); + + FirstStartup.init(true /* newProfile */); + await submissionPromise; + + Assert.ok( + errorLogged, + "An error should have been logged when trying to run after initialization" + ); + + // The add-on should NOT have been installed since we were too late + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + version: BUILTIN_ADDON_VERSION, + }); + + sandbox.restore(); + await nimbusFeatureCleanup(); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); + } +); + +/** + * Test that firstStartupNewProfile doesn't run when the category tasks pref + * is disabled. + */ +add_task( + { skip_if: () => !AppConstants.MOZ_NORMANDY }, + async function test_firstStartup_category_disabled() { + // Disable category tasks + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, false); + FirstStartup.resetForTesting(); + + // Reset AboutNewTabResourceMapping state + mockAboutNewTabUninit(); + + const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.789`; + + const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ + updateAddonVersion, + }); + + let sandbox = sinon.createSandbox(); + let firstStartupNewProfileSpy = sandbox.spy( + AboutNewTabResourceMapping, + "firstStartupNewProfile" + ); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + resolve(); + }); + }); + + FirstStartup.init(true /* newProfile */); + await submissionPromise; + + Assert.ok( + !firstStartupNewProfileSpy.called, + "firstStartupNewProfile should not have been called when pref is disabled" + ); + + // The add-on should still be the builtin version + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + version: BUILTIN_ADDON_VERSION, + }); + + sandbox.restore(); + await nimbusFeatureCleanup(); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); + } +); + +/** + * Test that if AboutNewTabResourceMapping.init() is called after the XPI + * download has started but before onInstallPostponed is called, we skip + * attempting to force the restartless install and fall back to a staged + * install instead. + */ +add_task( + { skip_if: () => !AppConstants.MOZ_NORMANDY }, + async function test_firstStartup_init_during_download() { + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); + FirstStartup.resetForTesting(); + + // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run + mockAboutNewTabUninit(); + + assertNewTabResourceMapping(); + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + }); + + const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.999`; + + const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ + updateAddonVersion, + }); + assertTrainhopAddonVersionPref(updateAddonVersion); + + // Stub updateTrainhopAddonState to call init() in the middle of its execution + let sandbox = sinon.createSandbox(); + let aboutNewTabInitSpy = sandbox.spy(AboutNewTabResourceMapping, "init"); + + let originalUpdateTrainhopAddonState = + AboutNewTabResourceMapping.updateTrainhopAddonState.bind( + AboutNewTabResourceMapping + ); + + let updateTrainhopStarted = false; + sandbox + .stub(AboutNewTabResourceMapping, "updateTrainhopAddonState") + .callsFake(async function (forceRestartlessInstall) { + updateTrainhopStarted = true; + + // Start the update process + let updatePromise = originalUpdateTrainhopAddonState( + forceRestartlessInstall + ); + + // Call init immediately after starting the update, simulating + // the browser window opening during the XPI download + info( + "Calling AboutNewTabResourceMapping.init() during updateTrainhopAddonState" + ); + AboutNewTabResourceMapping.init(); + + // Wait for the update to complete + await updatePromise; + }); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); + resolve(); + }); + }); + + FirstStartup.init(true /* newProfile */); + await submissionPromise; + + Assert.ok( + updateTrainhopStarted, + "updateTrainhopAddonState should have started" + ); + Assert.ok( + aboutNewTabInitSpy.calledOnce, + "AboutNewTabResourceMapping.init should have been called" + ); + + // The add-on should be staged for install, not installed restartlessly + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + version: BUILTIN_ADDON_VERSION, + }); + + // Verify there's a pending install + const pendingInstall = (await AddonManager.getAllInstalls()).find( + install => install.addon.id === BUILTIN_ADDON_ID + ); + Assert.ok(pendingInstall, "Should have a pending install"); + Assert.equal( + pendingInstall.state, + AddonManager.STATE_POSTPONED, + "Install should be postponed" + ); + Assert.equal( + pendingInstall.addon.version, + updateAddonVersion, + "Pending install should be for the train-hop version" + ); + + // Clean up + await cancelPendingInstall(pendingInstall); + sandbox.restore(); + await nimbusFeatureCleanup(); + assertTrainhopAddonVersionPref(""); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); + } +); + +/** + * Test that the TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID Nimbus feature can be + * used to remotely disable the FirstStartup force-install flow. + */ +add_task( + { skip_if: () => !AppConstants.MOZ_NORMANDY }, + async function test_firstStartup_remote_disable() { + // Enable category tasks for first startup + Services.prefs.setBoolPref(PREF_CATEGORY_TASKS, true); + FirstStartup.resetForTesting(); + + // Reset AboutNewTabResourceMapping state so firstStartupNewProfile can run + mockAboutNewTabUninit(); + + // Sanity check - verify built-in add-on resources have been mapped + assertNewTabResourceMapping(); + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + }); + assertTrainhopAddonNimbusExposure({ expectedExposure: false }); + + const updateAddonVersion = `${BUILTIN_ADDON_VERSION}.123`; + + const { nimbusFeatureCleanup } = await setupNimbusTrainhopAddon({ + updateAddonVersion, + }); + assertTrainhopAddonVersionPref(updateAddonVersion); + + const firstStartupFeatureCleanup = + await NimbusTestUtils.enrollWithFeatureConfig( + { + featureId: TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID, + value: { enabled: false }, + }, + { isRollout: true } + ); + + // Track whether firstStartupNewProfile was called + let sandbox = sinon.createSandbox(); + let firstStartupNewProfileSpy = sandbox.spy( + AboutNewTabResourceMapping, + "firstStartupNewProfile" + ); + + let submissionPromise = new Promise(resolve => { + GleanPings.firstStartup.testBeforeNextSubmit(() => { + Assert.equal(FirstStartup.state, FirstStartup.SUCCESS); + resolve(); + }); + }); + + // Run FirstStartup which should trigger our category hook + FirstStartup.init(true /* newProfile */); + + await submissionPromise; + + Assert.ok( + firstStartupNewProfileSpy.calledOnce, + "firstStartupNewProfile should have been called" + ); + + // The add-on should still be the builtin version + await asyncAssertNewTabAddon({ + locationName: BUILTIN_LOCATION_NAME, + version: BUILTIN_ADDON_VERSION, + }); + + sandbox.restore(); + await nimbusFeatureCleanup(); + await firstStartupFeatureCleanup(); + assertTrainhopAddonVersionPref(""); + Services.prefs.clearUserPref(PREF_CATEGORY_TASKS); + } +); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -24,5 +24,8 @@ skip-if = [ "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # Bug 1989373 ] +["test_nimbus_newtabTrainhopAddon_firstStartup.js"] +head = "../../../../extensions/newtab/test/xpcshell/head.js head_nimbus_trainhop.js" + ["test_nimbus_newtabTrainhopAddon_onBrowserReady.js"] head = "../../../../extensions/newtab/test/xpcshell/head.js head_nimbus_trainhop.js" diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml @@ -1611,6 +1611,20 @@ newtabTrainhopAddon: branch: user pref: browser.newtabpage.trainhopAddon.version +newtabTrainhopFirstStartup: + description: | + Controls startup behaviours if and when a train-hop is available for a + fresh install. + owner: mconley@mozilla.com + hasExposure: false + variables: + enabled: + type: boolean + description: | + True if New Tab should attempt to download, install and enable + train-hopped newtab XPIs for the first profile of new installs on + Windows. This is true by default, but can be set to false to disable. + newtabTopicSelection: description: the about:newtab topic selection experience. owner: nbarrett@mozilla.com