tor-browser

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

commit f388efa30e9c2d42d2b8e50c32eacf1dbe6d747e
parent 4d2d424e3c1e9fc5df7162cd85d413627998727b
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Wed,  5 Nov 2025 10:27:40 +0000

Bug 1996631 - Improve testability of DefaultLocaleSettingsController r=android-reviewers,anpopa,avirvara

This patch refactors the `DefaultLocaleSettingsController` to improve its testability and code clarity. The key changes include:

- Extracting the logic for recreating the activity into a new `recreateActivity()` function.
- Moving the calls to `LocaleManager.setNewLocale` and `LocaleManager.resetToSystemDefault` into new `updateLocale()` and `resetToSystemDefault()` wrapper functions.
- Updating `LocaleSettingsControllerTest` to use Robolectric for a more realistic `Activity` context and adapting the tests to verify the behavior of the newly extracted methods instead of mocking static `LocaleManager` calls.

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

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt | 93+++++++++++++++++++++++--------------------------------------------------------
Mmobile/android/fenix/config/detekt-baseline.xml | 5-----
3 files changed, 99 insertions(+), 85 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.settings.advanced import android.app.Activity import android.content.Context import android.os.Build +import androidx.annotation.VisibleForTesting import mozilla.components.browser.state.action.SearchAction import mozilla.components.browser.state.store.BrowserStore import mozilla.components.support.locale.LocaleManager @@ -14,12 +15,49 @@ import mozilla.components.support.locale.LocaleUseCases import org.mozilla.fenix.nimbus.FxNimbus import java.util.Locale +/** + * Controller responsible for handling user interactions on the locale settings screen. + * This includes selecting a new locale, searching for a locale, and resetting to the + * system default locale. + */ interface LocaleSettingsController { + + /** + * Handles the selection of a new locale. + * + * @param locale The [Locale] selected by the user. + */ fun handleLocaleSelected(locale: Locale) + + /** + * Handles user input in the locale search field. + * + * @param query The search string typed by the user. + */ fun handleSearchQueryTyped(query: String) + + /** + * Handles the selection of the system's default locale. + */ fun handleDefaultLocaleSelected() } +/** + * Default implementation of [LocaleSettingsController]. + * + * This class manages the logic for changing the application's language. It handles user interactions + * from the locale settings screen, such as selecting a new language or resetting to the system default. + * It coordinates with various stores and use cases to update the application state, refresh + * related components like search engines, and apply the new locale by recreating the activity. + * + * @param activity The current [Activity] context, required for recreating the UI and accessing resources. + * @param localeSettingsStore The store that manages the state for the locale settings UI, such as the + * list of available locales and the current search query. + * @param browserStore The main browser store, used here to dispatch actions like refreshing search + * engines when the locale changes. + * @param localeUseCase A set of use cases for managing the application's locale, including setting a new + * locale or resetting to the system default. + */ class DefaultLocaleSettingsController( private val activity: Activity, private val localeSettingsStore: LocaleSettingsStore, @@ -35,18 +73,12 @@ class DefaultLocaleSettingsController( } localeSettingsStore.dispatch(LocaleSettingsAction.Select(locale)) browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) - LocaleManager.setNewLocale(activity, localeUseCase, locale) - LocaleManager.updateBaseConfiguration(activity, locale) + updateLocale(locale) + updateBaseConfiguration(activity, locale) // Invalidate cached values to use the new locale FxNimbus.features.nimbusValidation.withCachedValue(null) - activity.recreate() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0) - } else { - @Suppress("DEPRECATION") - activity.overridePendingTransition(0, 0) - } + recreateActivity() } override fun handleDefaultLocaleSelected() { @@ -55,11 +87,23 @@ class DefaultLocaleSettingsController( } localeSettingsStore.dispatch(LocaleSettingsAction.Select(localeSettingsStore.state.localeList[0])) browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) - LocaleManager.resetToSystemDefault(activity, localeUseCase) - LocaleManager.updateBaseConfiguration(activity, localeSettingsStore.state.localeList[0]) + resetToSystemDefault() + updateBaseConfiguration(activity, localeSettingsStore.state.localeList[0]) // Invalidate cached values to use the default locale FxNimbus.features.nimbusValidation.withCachedValue(null) + recreateActivity() + } + + override fun handleSearchQueryTyped(query: String) { + localeSettingsStore.dispatch(LocaleSettingsAction.Search(query)) + } + + /** + * Recreates the activity to apply locale changes and provides a smooth, instant transition. + */ + @VisibleForTesting + internal fun recreateActivity() { activity.recreate() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0) @@ -69,15 +113,29 @@ class DefaultLocaleSettingsController( } } - override fun handleSearchQueryTyped(query: String) { - localeSettingsStore.dispatch(LocaleSettingsAction.Search(query)) + /** + * Sets the new application locale and updates the base configuration to apply it. + * + * @param locale The new [Locale] to set. + */ + @VisibleForTesting + internal fun updateLocale(locale: Locale) { + LocaleManager.setNewLocale(activity, localeUseCase, locale) + } + + /** + * Resets the application's locale to the system's default locale. + */ + @VisibleForTesting + internal fun resetToSystemDefault() { + LocaleManager.resetToSystemDefault(activity, localeUseCase) } /** * Update the locale for the configuration of the app context's resources */ @Suppress("Deprecation") - fun LocaleManager.updateBaseConfiguration(context: Context, locale: Locale) { + fun updateBaseConfiguration(context: Context, locale: Locale) { val resources = context.applicationContext.resources val config = resources.configuration config.setLocale(locale) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt @@ -5,12 +5,11 @@ package org.mozilla.fenix.settings.advanced import android.app.Activity +import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify import io.mockk.verifyAll @@ -20,15 +19,19 @@ import mozilla.components.support.locale.LocaleManager import mozilla.components.support.locale.LocaleUseCases import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric import java.util.Locale +@RunWith(AndroidJUnit4::class) class LocaleSettingsControllerTest { - private val activity = mockk<Activity>(relaxed = true) + val activity: Activity = Robolectric.buildActivity(Activity::class.java).setup().get() + private val localeSettingsStore: LocaleSettingsStore = mockk(relaxed = true) private val browserStore: BrowserStore = mockk(relaxed = true) private val localeUseCases: LocaleUseCases = mockk(relaxed = true) - private val mockState = LocaleSettingsState(mockk(), mockk(), mockk()) + private val mockState = LocaleSettingsState(emptyList(), emptyList(), mockk()) private lateinit var controller: DefaultLocaleSettingsController @@ -43,28 +46,24 @@ class LocaleSettingsControllerTest { ), ) - mockkObject(LocaleManager) - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") + every { localeUseCases.notifyLocaleChanged(any()) } just Runs } @Test fun `don't set locale if same locale is chosen`() { val selectedLocale = Locale.Builder().setLanguage("en").setRegion("UK").build() every { localeSettingsStore.state } returns mockState.copy(selectedLocale = selectedLocale) - every { LocaleManager.getCurrentLocale(activity) } returns mockk() + + LocaleManager.setNewLocale(activity, locale = selectedLocale) controller.handleLocaleSelected(selectedLocale) verifyAll(inverse = true) { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) - LocaleManager.setNewLocale(activity, locale = selectedLocale) - activity.recreate() - } - with(controller) { - verify(inverse = true) { - LocaleManager.updateBaseConfiguration(activity, selectedLocale) - } + controller.updateLocale(selectedLocale) + controller.recreateActivity() + controller.updateBaseConfiguration(activity, selectedLocale) } } @@ -72,100 +71,62 @@ class LocaleSettingsControllerTest { fun `set a new locale from the list if other locale is chosen`() { val selectedLocale = Locale.Builder().setLanguage("en").setRegion("UK").build() val otherLocale: Locale = mockk() - every { localeUseCases.notifyLocaleChanged } returns mockk() every { localeSettingsStore.state } returns mockState.copy(selectedLocale = otherLocale) - every { LocaleManager.setNewLocale(activity, localeUseCases, selectedLocale) } returns activity - with(controller) { - every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs - } controller.handleLocaleSelected(selectedLocale) verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } verify { browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) } - verify { LocaleManager.setNewLocale(activity, localeUseCases, selectedLocale) } - verify { activity.recreate() } - verify { - @Suppress("DEPRECATION") - activity.overridePendingTransition(0, 0) - } - - with(controller) { - verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } - } + controller.updateLocale(selectedLocale) + controller.recreateActivity() + verify { controller.updateBaseConfiguration(activity, selectedLocale) } } @Test fun `set a new locale from the list if default locale is not selected`() { val selectedLocale = Locale.Builder().setLanguage("en").setRegion("UK").build() - every { localeUseCases.notifyLocaleChanged } returns mockk() every { localeSettingsStore.state } returns mockState.copy(selectedLocale = selectedLocale) - every { LocaleManager.getCurrentLocale(activity) } returns null - every { LocaleManager.setNewLocale(activity, localeUseCases, selectedLocale) } returns activity - with(controller) { - every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs - } + LocaleManager.setNewLocale(activity, locale = null) controller.handleLocaleSelected(selectedLocale) verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } verify { browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) } - verify { LocaleManager.setNewLocale(activity, localeUseCases, selectedLocale) } - verify { activity.recreate() } - verify { - @Suppress("DEPRECATION") - activity.overridePendingTransition(0, 0) - } - - with(controller) { - verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } - } + verify { controller.updateLocale(selectedLocale) } + verify { controller.recreateActivity() } + verify { controller.updateBaseConfiguration(activity, selectedLocale) } } @Test fun `don't set default locale if default locale is already chosen`() { val selectedLocale = Locale.Builder().setLanguage("en").setRegion("UK").build() every { localeSettingsStore.state } returns mockState.copy(localeList = listOf(selectedLocale)) - every { LocaleManager.getCurrentLocale(activity) } returns null + LocaleManager.setNewLocale(activity, locale = null) controller.handleDefaultLocaleSelected() verifyAll(inverse = true) { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) - LocaleManager.resetToSystemDefault(activity, localeUseCases) - activity.recreate() - with(controller) { - LocaleManager.updateBaseConfiguration(activity, selectedLocale) - } + controller.resetToSystemDefault() + controller.recreateActivity() + controller.updateBaseConfiguration(activity, selectedLocale) } } @Test fun `set the default locale as the new locale`() { val selectedLocale = Locale.Builder().setLanguage("en").setRegion("UK").build() - every { localeUseCases.notifyLocaleChanged } returns mockk() every { localeSettingsStore.state } returns mockState.copy(localeList = listOf(selectedLocale)) - every { LocaleManager.resetToSystemDefault(activity, localeUseCases) } just Runs - with(controller) { - every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs - } controller.handleDefaultLocaleSelected() verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } verify { browserStore.dispatch(SearchAction.RefreshSearchEnginesAction) } - verify { LocaleManager.resetToSystemDefault(activity, localeUseCases) } - verify { activity.recreate() } - verify { - @Suppress("DEPRECATION") - activity.overridePendingTransition(0, 0) - } - - with(controller) { - verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } - } + verify { controller.resetToSystemDefault() } + verify { controller.recreateActivity() } + verify { controller.updateBaseConfiguration(activity, selectedLocale) } } @Test diff --git a/mobile/android/fenix/config/detekt-baseline.xml b/mobile/android/fenix/config/detekt-baseline.xml @@ -271,8 +271,6 @@ <ID>UndocumentedPublicClass:DebugDrawerRoute.kt$DebugDrawerRoute$Companion</ID> <ID>UndocumentedPublicClass:DebugFenixApplication.kt$DebugFenixApplication : FenixApplication</ID> <ID>UndocumentedPublicClass:DefaultBrowserPreference.kt$DefaultBrowserPreference : Preference</ID> - <ID>UndocumentedPublicClass:DefaultLocaleSettingsController.kt$DefaultLocaleSettingsController : LocaleSettingsController</ID> - <ID>UndocumentedPublicClass:DefaultLocaleSettingsController.kt$LocaleSettingsController</ID> <ID>UndocumentedPublicClass:DefaultSyncController.kt$SyncController</ID> <ID>UndocumentedPublicClass:DefaultSyncInteractor.kt$SyncInteractor</ID> <ID>UndocumentedPublicClass:DefaultTopSitesView.kt$DefaultTopSitesView : TopSitesView</ID> @@ -667,9 +665,6 @@ <ID>UndocumentedPublicFunction:CounterPreference.kt$CounterPreference$fun underMaxCount()</ID> <ID>UndocumentedPublicFunction:CreditCardItemViewHolder.kt$CreditCardItemViewHolder$fun bind(creditCard: CreditCard)</ID> <ID>UndocumentedPublicFunction:DefaultBrowserPreference.kt$DefaultBrowserPreference$fun updateSwitch()</ID> - <ID>UndocumentedPublicFunction:DefaultLocaleSettingsController.kt$LocaleSettingsController$fun handleDefaultLocaleSelected()</ID> - <ID>UndocumentedPublicFunction:DefaultLocaleSettingsController.kt$LocaleSettingsController$fun handleLocaleSelected(locale: Locale)</ID> - <ID>UndocumentedPublicFunction:DefaultLocaleSettingsController.kt$LocaleSettingsController$fun handleSearchQueryTyped(query: String)</ID> <ID>UndocumentedPublicFunction:DefaultSyncController.kt$SyncController$fun handleCameraPermissionsNeeded()</ID> <ID>UndocumentedPublicFunction:DefaultSyncInteractor.kt$SyncInteractor$fun onCameraPermissionsNeeded()</ID> <ID>UndocumentedPublicFunction:DefaultToolbarMenu.kt$DefaultToolbarMenu$@VisibleForTesting(otherwise = PRIVATE) fun canAddToHomescreen(): Boolean</ID>