tor-browser

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

commit 1d6c9204d4514de994d558529db4e5617ba114ff
parent 1f15a177ae062fa0a6b821ffe3ae99b1c8449f9e
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Fri, 19 Dec 2025 13:53:36 +0000

Bug 2007095 - Update VoiceSearchFeature to support CoroutineDispatcher injection r=android-reviewers,pollymce

Modified `VoiceSearchFeature` to accept an optional `CoroutineDispatcher`, defaulting to `Dispatchers.Main`. This allows for better control over coroutine execution in tests.

Updated `VoiceSearchFeatureTest` to:
- Use `StandardTestDispatcher` and `runTest` instead of `MainCoroutineRule` and `runTestOnMain`.
- Inject the test dispatcher into `VoiceSearchFeature`.
- Use `testDispatcher.scheduler.advanceUntilIdle()` to ensure asynchronous actions are completed before verification.

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

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/VoiceSearchFeature.kt | 5++++-
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/VoiceSearchFeatureTest.kt | 22+++++++++++-----------
2 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/VoiceSearchFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/VoiceSearchFeature.kt @@ -12,7 +12,9 @@ import android.speech.RecognizerIntent import androidx.activity.result.ActivityResultLauncher import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.map @@ -37,6 +39,7 @@ class VoiceSearchFeature( private val context: Context, private val appStore: AppStore, private val voiceSearchLauncher: ActivityResultLauncher<Intent>, + private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, ) : LifecycleAwareFeature { private var scope: CoroutineScope? = null @@ -51,7 +54,7 @@ class VoiceSearchFeature( } private fun observeVoiceSearchRequests() { - scope = appStore.flowScoped { flow -> + scope = appStore.flowScoped(dispatcher = mainDispatcher) { flow -> flow.map { state -> state.voiceSearchState } .distinctUntilChangedBy { it.isRequestingVoiceInput } .collect { voiceSearchState -> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/VoiceSearchFeatureTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/VoiceSearchFeatureTest.kt @@ -13,11 +13,10 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.components.appstate.VoiceSearchAction @@ -26,12 +25,11 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class VoiceSearchFeatureTest { - @get:Rule - val mainCoroutineRule = MainCoroutineRule() - + private val testDispatcher = StandardTestDispatcher() private val appStore = spyk(AppStore()) private val voiceSearchLauncher: ActivityResultLauncher<Intent> = mockk(relaxed = true) - private val feature = VoiceSearchFeature(testContext, appStore, voiceSearchLauncher) + private val feature = + VoiceSearchFeature(testContext, appStore, voiceSearchLauncher, testDispatcher) @Before fun setup() { @@ -39,25 +37,27 @@ class VoiceSearchFeatureTest { } @Test - fun `GIVEN a voice input request WHEN no activity is available to handle it THEN dispatches return a null result`() = runTestOnMain { + fun `GIVEN a voice input request WHEN no activity is available to handle it THEN dispatches return a null result`() = runTest(testDispatcher) { every { voiceSearchLauncher.launch(any()) } throws ActivityNotFoundException() appStore.dispatch(VoiceInputRequested) + testDispatcher.scheduler.advanceUntilIdle() verify { appStore.dispatch(VoiceSearchAction.VoiceInputResultReceived(null)) } } @Test - fun `GIVEN SecurityException WHEN launching voice search THEN dispatches VoiceInputResultReceived null`() = runTestOnMain { + fun `GIVEN SecurityException WHEN launching voice search THEN dispatches VoiceInputResultReceived null`() = runTest(testDispatcher) { every { voiceSearchLauncher.launch(any()) } throws SecurityException() appStore.dispatch(VoiceSearchAction.VoiceInputRequested) + testDispatcher.scheduler.advanceUntilIdle() verify { appStore.dispatch(VoiceSearchAction.VoiceInputResultReceived(null)) } } @Test - fun `GIVEN successful result WHEN handleVoiceSearchResult is called THEN dispatches VoiceInputResultReceived with search terms`() = runTestOnMain { + fun `GIVEN successful result WHEN handleVoiceSearchResult is called THEN dispatches VoiceInputResultReceived with search terms`() = runTest(testDispatcher) { val intent = mockk<Intent>() val results = arrayListOf("search term") every { intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) } returns results @@ -68,7 +68,7 @@ class VoiceSearchFeatureTest { } @Test - fun `GIVEN cancelled or failed result WHEN handleVoiceSearchResult is called THEN dispatches VoiceInputResultReceived with null`() = runTestOnMain { + fun `GIVEN cancelled or failed result WHEN handleVoiceSearchResult is called THEN dispatches VoiceInputResultReceived with null`() = runTest(testDispatcher) { feature.handleVoiceSearchResult(Activity.RESULT_CANCELED, null) verify { appStore.dispatch(VoiceSearchAction.VoiceInputResultReceived(null)) } }