tor-browser

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

commit 233c97df8abc6eef46716129e21da9c5343a2836
parent bc4dae42f9df10e46aa3dd74e6dc1a16ec2bb0b7
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Wed, 31 Dec 2025 09:12:57 +0000

Bug 2007669 - Refactor ShareController and tests to use modern coroutine testing tools. r=android-reviewers,giorga

This change updates `ShareController` and its corresponding tests to improve coroutine handling and test reliability.

- Replaced `MainCoroutineRule` and `runTestOnMain` with `StandardTestDispatcher` and `runTest` in `ShareControllerTest`.
- Updated `ShareController` to accept both `mainDispatcher` and `ioDispatcher` for better control over execution contexts.
- Updated all test cases to use the new `runTest(testDispatcher)` pattern and explicit scheduler advancement where necessary.
- Simplified test setup by removing unnecessary `spyk` on `testContext`.

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

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt | 10++++++----
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt | 1217+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
2 files changed, 758 insertions(+), 469 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -87,7 +87,8 @@ interface ShareController { * @param navController [NavController] used for navigation. * @param recentAppsStorage Instance of [RecentAppsStorage] for storing and retrieving the most recent apps. * @param viewLifecycleScope [CoroutineScope] used for retrieving the most recent apps in the background. - * @param dispatcher Dispatcher used to execute suspending functions. + * @param mainDispatcher Dispatcher for executing tasks on the Main thread. + * @param ioDispatcher Dispatcher for executing I/O-bound tasks, like updating local storage. * @param fxaEntrypoint The entrypoint if we need to authenticate, it will be reported in telemetry. * @param dismiss Callback signalling sharing can be closed. */ @@ -104,7 +105,8 @@ class DefaultShareController( private val navController: NavController, private val recentAppsStorage: RecentAppsStorage, private val viewLifecycleScope: CoroutineScope, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, + private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val fxaEntrypoint: FxAEntryPoint = FenixFxAEntryPoint.ShareMenu, private val dismiss: (ShareController.Result) -> Unit, ) : ShareController { @@ -135,7 +137,7 @@ class DefaultShareController( return } - viewLifecycleScope.launch(dispatcher) { + viewLifecycleScope.launch(ioDispatcher) { recentAppsStorage.updateRecentApp(app.activityName) } @@ -218,7 +220,7 @@ class DefaultShareController( shareOperation: () -> Deferred<Boolean>, ) { // Use GlobalScope to allow the continuation of this method even if the share fragment is closed. - GlobalScope.launch(Dispatchers.Main) { + GlobalScope.launch(mainDispatcher) { val result = if (shareOperation.invoke().await()) { showSuccess(destination) ShareController.Result.SUCCESS diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt @@ -17,12 +17,11 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.slot -import io.mockk.spyk import io.mockk.verify import io.mockk.verifyOrder import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.DeviceType @@ -31,8 +30,6 @@ import mozilla.components.feature.accounts.push.SendTabUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.share.RecentAppsStorage import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -52,8 +49,6 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class ShareControllerTest { - // Need a valid context to retrieve Strings for example, but we also need it to return our "metrics" - private val context: Context = spyk(testContext) private val appStore: AppStore = mockk(relaxed = true) private val shareSubject = "shareSubject" private val shareData = listOf( @@ -82,525 +77,760 @@ class ShareControllerTest { @get:Rule val gleanTestRule = FenixGleanTestRule(testContext) - @get:Rule - val coroutinesTestRule = MainCoroutineRule() - private val testDispatcher = coroutinesTestRule.testDispatcher - private val testCoroutineScope = coroutinesTestRule.scope - private val controller = DefaultShareController( - context, appStore, shareSubject, shareData, sendTabUseCases, saveToPdfUseCase, printUseCase, sentFromFirefoxManager, - navController, recentAppStorage, testCoroutineScope, testDispatcher, FenixFxAEntryPoint.ShareMenu, dismiss, - ) + private val testDispatcher = StandardTestDispatcher() @Test - fun `handleShareClosed should call a passed in delegate to close this`() { - controller.handleShareClosed() + fun `handleShareClosed should call a passed in delegate to close this`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - verify { dismiss(ShareController.Result.DISMISSED) } - } + controller.handleShareClosed() - @OptIn(ExperimentalCoroutinesApi::class) // advanceUntilIdle - @Test - fun `handleShareToApp should start a new sharing activity and close this`() = runTestOnMain { - assertNull(Events.shareToApp.testGetValue()) - - val appPackageName = "package" - val appClassName = "activity" - val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) - val shareIntent = slot<Intent>() - // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call - // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we - // need to use an Activity Context. - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - activityContext, appStore, shareSubject, shareData, mockk(), mockk(), - mockk(), sentFromFirefoxManager, mockk(), recentAppStorage, testCoroutineScope, testDispatcher, - FenixFxAEntryPoint.ShareMenu, dismiss, - ) - every { activityContext.startActivity(capture(shareIntent)) } just Runs - every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs - every { sentFromFirefoxManager.maybeAppendShareText(any(), any()) } returns textToShare - - testController.handleShareToApp(appShareOption) - advanceUntilIdle() - - assertEquals("shareToApp event only called once", 1, Events.shareToApp.testGetValue()?.size) - assertEquals("other", Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package")) - - // Check that the Intent used for querying apps has the expected structure - assertTrue(shareIntent.isCaptured) - assertEquals(Intent.ACTION_SEND, shareIntent.captured.action) - @Suppress("DEPRECATION") - assertEquals(shareSubject, shareIntent.captured.extras!![Intent.EXTRA_SUBJECT]) - @Suppress("DEPRECATION") - assertEquals(textToShare, shareIntent.captured.extras!![Intent.EXTRA_TEXT]) - assertEquals("text/plain", shareIntent.captured.type) - assertEquals(Intent.FLAG_ACTIVITY_NEW_DOCUMENT + Intent.FLAG_ACTIVITY_MULTIPLE_TASK, shareIntent.captured.flags) - assertEquals(appPackageName, shareIntent.captured.component!!.packageName) - assertEquals(appClassName, shareIntent.captured.component!!.className) - - verify { recentAppStorage.updateRecentApp(appShareOption.activityName) } - verifyOrder { - activityContext.startActivity(shareIntent.captured) - dismiss(ShareController.Result.SUCCESS) + verify { dismiss(ShareController.Result.DISMISSED) } } - } @Test - fun `handleShareToApp should record to telemetry packages which are in allowed list`() { - assertNull(Events.shareToApp.testGetValue()) - - val appPackageName = "com.android.bluetooth" - val appClassName = "activity" - val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) - val shareIntent = slot<Intent>() - // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call - // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we - // need to use an Activity Context. - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - activityContext, appStore, shareSubject, shareData, mockk(), mockk(), - mockk(), sentFromFirefoxManager, mockk(), recentAppStorage, testCoroutineScope, testDispatcher, - FenixFxAEntryPoint.ShareMenu, dismiss, - ) + fun `handleShareToApp should start a new sharing activity and close this`() = + runTest(testDispatcher) { + assertNull(Events.shareToApp.testGetValue()) + + val appPackageName = "package" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot<Intent>() + // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call + // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we + // need to use an Activity Context. + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + activityContext, + appStore, + shareSubject, + shareData, + mockk(), + mockk(), + mockk(), + sentFromFirefoxManager, + mockk(), + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + every { activityContext.startActivity(capture(shareIntent)) } just Runs + every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs + every { sentFromFirefoxManager.maybeAppendShareText(any(), any()) } returns textToShare - every { activityContext.startActivity(capture(shareIntent)) } just Runs - every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs + testController.handleShareToApp(appShareOption) + testDispatcher.scheduler.advanceUntilIdle() - testController.handleShareToApp(appShareOption) + assertEquals( + "shareToApp event only called once", + 1, + Events.shareToApp.testGetValue()?.size, + ) + assertEquals( + "other", + Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package"), + ) - assertEquals("shareToApp event only called once", 1, Events.shareToApp.testGetValue()?.size) - assertEquals("com.android.bluetooth", Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package")) - } + // Check that the Intent used for querying apps has the expected structure + assertTrue(shareIntent.isCaptured) + assertEquals(Intent.ACTION_SEND, shareIntent.captured.action) + @Suppress("DEPRECATION") + assertEquals(shareSubject, shareIntent.captured.extras!![Intent.EXTRA_SUBJECT]) + @Suppress("DEPRECATION") + assertEquals(textToShare, shareIntent.captured.extras!![Intent.EXTRA_TEXT]) + assertEquals("text/plain", shareIntent.captured.type) + assertEquals( + Intent.FLAG_ACTIVITY_NEW_DOCUMENT + Intent.FLAG_ACTIVITY_MULTIPLE_TASK, + shareIntent.captured.flags, + ) + assertEquals(appPackageName, shareIntent.captured.component!!.packageName) + assertEquals(appClassName, shareIntent.captured.component!!.className) + + verify { recentAppStorage.updateRecentApp(appShareOption.activityName) } + verifyOrder { + activityContext.startActivity(shareIntent.captured) + dismiss(ShareController.Result.SUCCESS) + } + } @Test - fun `handleShareToApp should record to telemetry as other when app package not in allowed list`() { - assertNull(Events.shareToApp.testGetValue()) - - val appPackageName = "com.package.record.not.allowed" - val appClassName = "activity" - val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) - val shareIntent = slot<Intent>() - // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call - // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we - // need to use an Activity Context. - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - activityContext, appStore, shareSubject, shareData, mockk(), mockk(), - mockk(), sentFromFirefoxManager, mockk(), recentAppStorage, testCoroutineScope, testDispatcher, - FenixFxAEntryPoint.ShareMenu, dismiss, - ) + fun `handleShareToApp should record to telemetry packages which are in allowed list`() = + runTest(testDispatcher) { + assertNull(Events.shareToApp.testGetValue()) + + val appPackageName = "com.android.bluetooth" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot<Intent>() + // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call + // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we + // need to use an Activity Context. + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + activityContext, + appStore, + shareSubject, + shareData, + mockk(), + mockk(), + mockk(), + sentFromFirefoxManager, + mockk(), + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - every { activityContext.startActivity(capture(shareIntent)) } just Runs - every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs + every { activityContext.startActivity(capture(shareIntent)) } just Runs + every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs - testController.handleShareToApp(appShareOption) + testController.handleShareToApp(appShareOption) - // Only called once and package is not in the allowed telemetry list so this should record "other" - assertEquals("shareToApp event only called once", 1, Events.shareToApp.testGetValue()?.size) - assertEquals("other", Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package")) - } + assertEquals( + "shareToApp event only called once", + 1, + Events.shareToApp.testGetValue()?.size, + ) + assertEquals( + "com.android.bluetooth", + Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package"), + ) + } @Test - fun `handleShareToApp should dismiss with an error start when a security exception occurs`() { - val appPackageName = "package" - val appClassName = "activity" - val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) - val shareIntent = slot<Intent>() - // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call - // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we - // need to use an Activity Context. - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = shareSubject, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) - every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs - every { activityContext.startActivity(capture(shareIntent)) } throws SecurityException() - every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app" + fun `handleShareToApp should record to telemetry as other when app package not in allowed list`() = + runTest(testDispatcher) { + assertNull(Events.shareToApp.testGetValue()) + + val appPackageName = "com.package.record.not.allowed" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot<Intent>() + // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call + // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we + // need to use an Activity Context. + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + activityContext, + appStore, + shareSubject, + shareData, + mockk(), + mockk(), + mockk(), + sentFromFirefoxManager, + mockk(), + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + + every { activityContext.startActivity(capture(shareIntent)) } just Runs + every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs - testController.handleShareToApp(appShareOption) + testController.handleShareToApp(appShareOption) - verifyOrder { - activityContext.startActivity(shareIntent.captured) - appStore.dispatch(ShareAction.ShareToAppFailed) - dismiss(ShareController.Result.SHARE_ERROR) + // Only called once and package is not in the allowed telemetry list so this should record "other" + assertEquals( + "shareToApp event only called once", + 1, + Events.shareToApp.testGetValue()?.size, + ) + assertEquals( + "other", + Events.shareToApp.testGetValue()?.last()?.extra?.getValue("app_package"), + ) } - } @Test - fun `handleShareToApp should dismiss with an error start when a ActivityNotFoundException occurs`() { - val appPackageName = "package" - val appClassName = "activity" - val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) - val shareIntent = slot<Intent>() - // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call - // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we - // need to use an Activity Context. - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = shareSubject, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) - every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs - every { activityContext.startActivity(capture(shareIntent)) } throws ActivityNotFoundException() - every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app" + fun `handleShareToApp should dismiss with an error start when a security exception occurs`() = + runTest(testDispatcher) { + val appPackageName = "package" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot<Intent>() + // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call + // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we + // need to use an Activity Context. + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = shareSubject, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) + every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs + every { activityContext.startActivity(capture(shareIntent)) } throws SecurityException() + every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app" - testController.handleShareToApp(appShareOption) + testController.handleShareToApp(appShareOption) - verifyOrder { - activityContext.startActivity(shareIntent.captured) - appStore.dispatch(ShareAction.ShareToAppFailed) - dismiss(ShareController.Result.SHARE_ERROR) + verifyOrder { + activityContext.startActivity(shareIntent.captured) + appStore.dispatch(ShareAction.ShareToAppFailed) + dismiss(ShareController.Result.SHARE_ERROR) + } } - } @Test - fun `WHEN handleSaveToPDF close the dialog and save the page to pdf`() { - val testController = DefaultShareController( - context = mockk(), - appStore = appStore, - shareSubject = shareSubject, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = saveToPdfUseCase, - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `handleShareToApp should dismiss with an error start when a ActivityNotFoundException occurs`() = + runTest(testDispatcher) { + val appPackageName = "package" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot<Intent>() + // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call + // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we + // need to use an Activity Context. + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = shareSubject, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) + every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs + every { activityContext.startActivity(capture(shareIntent)) } throws ActivityNotFoundException() + every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app" - testController.handleSaveToPDF("tabID") + testController.handleShareToApp(appShareOption) - verify { - saveToPdfUseCase.invoke("tabID") - dismiss(ShareController.Result.DISMISSED) + verifyOrder { + activityContext.startActivity(shareIntent.captured) + appStore.dispatch(ShareAction.ShareToAppFailed) + dismiss(ShareController.Result.SHARE_ERROR) + } } - } @Test - fun `WHEN handlePrint close the dialog and print the page AND send tapped telemetry`() { - val testController = DefaultShareController( - context = mockk(), - appStore = appStore, - shareSubject = shareSubject, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = printUseCase, - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `WHEN handleSaveToPDF close the dialog and save the page to pdf`() = + runTest(testDispatcher) { + val testController = DefaultShareController( + context = mockk(), + appStore = appStore, + shareSubject = shareSubject, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = saveToPdfUseCase, + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - testController.handlePrint("tabID") + testController.handleSaveToPDF("tabID") - verify { - printUseCase.invoke("tabID") - dismiss(ShareController.Result.DISMISSED) + verify { + saveToPdfUseCase.invoke("tabID") + dismiss(ShareController.Result.DISMISSED) + } } - assertNotNull(Events.shareMenuAction.testGetValue()) - val printTapped = Events.shareMenuAction.testGetValue()!! - assertEquals(1, printTapped.size) - assertEquals("print", printTapped.single().extra?.getValue("item")) - } - @Test - fun `getShareSubject should return the shareSubject when shareSubject is not null`() { - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = shareSubject, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `WHEN handlePrint close the dialog and print the page AND send tapped telemetry`() = + runTest(testDispatcher) { + val testController = DefaultShareController( + context = mockk(), + appStore = appStore, + shareSubject = shareSubject, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = printUseCase, + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - assertEquals(shareSubject, testController.getShareSubject()) - } + testController.handlePrint("tabID") - @Test - fun `getShareSubject should return a combination of non-null titles when shareSubject is null`() { - val activityContext: Context = mockk<Activity>() - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = null, - shareData = shareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + verify { + printUseCase.invoke("tabID") + dismiss(ShareController.Result.DISMISSED) + } - assertEquals("title0, title1", testController.getShareSubject()) - } + assertNotNull(Events.shareMenuAction.testGetValue()) + val printTapped = Events.shareMenuAction.testGetValue()!! + assertEquals(1, printTapped.size) + assertEquals("print", printTapped.single().extra?.getValue("item")) + } @Test - fun `getShareSubject should return just the not null titles string when shareSubject is null`() { - val activityContext: Context = mockk<Activity>() - val partialTitlesShareData = listOf( - ShareData(url = "url0", title = null), - ShareData(url = "url1", title = "title1"), - ) - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = null, - shareData = partialTitlesShareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `getShareSubject should return the shareSubject when shareSubject is not null`() = + runTest(testDispatcher) { + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = shareSubject, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - assertEquals("title1", testController.getShareSubject()) - } + assertEquals(shareSubject, testController.getShareSubject()) + } @Test - fun `getShareSubject should return empty string when shareSubject and all titles are null`() { - val activityContext: Context = mockk<Activity>() - val noTitleShareData = listOf( - ShareData(url = "url0", title = null), - ShareData(url = "url1", title = null), - ) - val testController = DefaultShareController( - context = activityContext, - appStore = appStore, - shareSubject = null, - shareData = noTitleShareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `getShareSubject should return a combination of non-null titles when shareSubject is null`() = + runTest(testDispatcher) { + val activityContext: Context = mockk<Activity>() + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = null, + shareData = shareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - assertEquals("", testController.getShareSubject()) - } + assertEquals("title0, title1", testController.getShareSubject()) + } @Test - fun `getShareSubject should return empty string when shareSubject is null and and all titles are empty`() { - val activityContext: Context = mockk<Activity>() - val noTitleShareData = listOf( - ShareData(url = "url0", title = ""), - ShareData(url = "url1", title = ""), - ) - val testController = DefaultShareController( - appStore = appStore, - context = activityContext, - shareSubject = null, - shareData = noTitleShareData, - sendTabUseCases = mockk(), - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = mockk(), - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `getShareSubject should return just the not null titles string when shareSubject is null`() = + runTest(testDispatcher) { + val activityContext: Context = mockk<Activity>() + val partialTitlesShareData = listOf( + ShareData(url = "url0", title = null), + ShareData(url = "url1", title = "title1"), + ) + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = null, + shareData = partialTitlesShareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - assertEquals("", testController.getShareSubject()) - } + assertEquals("title1", testController.getShareSubject()) + } @Test - @Suppress("DeferredResultUnused") - fun `handleShareToDevice should share to account device, inform callbacks and dismiss`() { - val deviceToShareTo = Device( - "deviceId", - "deviceName", - DeviceType.UNKNOWN, - false, - 0L, - emptyList(), - false, - null, - ) - val deviceId = slot<String>() - val tabsShared = slot<List<TabData>>() - - every { sendTabUseCases.sendToDeviceAsync(any(), any<List<TabData>>()) } returns CompletableDeferred(true) - every { navController.currentDestination?.id } returns R.id.shareFragment + fun `getShareSubject should return empty string when shareSubject and all titles are null`() = + runTest(testDispatcher) { + val activityContext: Context = mockk<Activity>() + val noTitleShareData = listOf( + ShareData(url = "url0", title = null), + ShareData(url = "url1", title = null), + ) + val testController = DefaultShareController( + context = activityContext, + appStore = appStore, + shareSubject = null, + shareData = noTitleShareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - controller.handleShareToDevice(deviceToShareTo) + assertEquals("", testController.getShareSubject()) + } - assertNotNull(SyncAccount.sendTab.testGetValue()) - assertEquals(1, SyncAccount.sendTab.testGetValue()!!.size) - assertNull(SyncAccount.sendTab.testGetValue()!!.single().extra) + @Test + fun `getShareSubject should return empty string when shareSubject is null and and all titles are empty`() = + runTest(testDispatcher) { + val activityContext: Context = mockk<Activity>() + val noTitleShareData = listOf( + ShareData(url = "url0", title = ""), + ShareData(url = "url1", title = ""), + ) + val testController = DefaultShareController( + appStore = appStore, + context = activityContext, + shareSubject = null, + shareData = noTitleShareData, + sendTabUseCases = mockk(), + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = mockk(), + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - verifyOrder { - sendTabUseCases.sendToDeviceAsync(capture(deviceId), capture(tabsShared)) - dismiss(ShareController.Result.SUCCESS) + assertEquals("", testController.getShareSubject()) } - assertTrue(deviceId.isCaptured) - assertEquals(deviceToShareTo.id, deviceId.captured) - assertTrue(tabsShared.isCaptured) - assertEquals(tabsData, tabsShared.captured) - } - @Test - @Suppress("DeferredResultUnused") - fun `handleShareToAllDevices calls handleShareToDevice multiple times`() { - every { sendTabUseCases.sendToAllAsync(any<List<TabData>>()) } returns CompletableDeferred(true) - every { navController.currentDestination?.id } returns R.id.shareFragment - - val devicesToShareTo = listOf( - Device( - "deviceId0", - "deviceName0", + fun `handleShareToDevice should share to account device, inform callbacks and dismiss`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + + val deviceToShareTo = Device( + "deviceId", + "deviceName", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null, - ), - Device( - "deviceId1", - "deviceName1", - DeviceType.UNKNOWN, - true, - 1L, - emptyList(), - false, - null, - ), - ) - val tabsShared = slot<List<TabData>>() - - controller.handleShareToAllDevices(devicesToShareTo) - - verifyOrder { - sendTabUseCases.sendToAllAsync(capture(tabsShared)) - dismiss(ShareController.Result.SUCCESS) + ) + val deviceId = slot<String>() + val tabsShared = slot<List<TabData>>() + + every { + sendTabUseCases.sendToDeviceAsync( + any(), + any<List<TabData>>(), + ) + } returns CompletableDeferred(true) + every { navController.currentDestination?.id } returns R.id.shareFragment + + controller.handleShareToDevice(deviceToShareTo) + testDispatcher.scheduler.advanceUntilIdle() + + assertNotNull(SyncAccount.sendTab.testGetValue()) + assertEquals(1, SyncAccount.sendTab.testGetValue()!!.size) + assertNull(SyncAccount.sendTab.testGetValue()!!.single().extra) + + verifyOrder { + sendTabUseCases.sendToDeviceAsync(capture(deviceId), capture(tabsShared)) + dismiss(ShareController.Result.SUCCESS) + } + + assertTrue(deviceId.isCaptured) + assertEquals(deviceToShareTo.id, deviceId.captured) + assertTrue(tabsShared.isCaptured) + assertEquals(tabsData, tabsShared.captured) } - // SendTabUseCases should send a the `shareTabs` mapped to tabData - assertTrue(tabsShared.isCaptured) - assertEquals(tabsData, tabsShared.captured) - } - @Test - fun `handleSignIn should navigate to the Sync Fragment and dismiss this one`() { - controller.handleSignIn() - - assertNotNull(SyncAccount.signInToSendTab.testGetValue()) - assertEquals(1, SyncAccount.signInToSendTab.testGetValue()!!.size) - assertNull(SyncAccount.signInToSendTab.testGetValue()!!.single().extra) + fun `handleShareToAllDevices calls handleShareToDevice multiple times`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - verifyOrder { - navController.navigate( - ShareFragmentDirections.actionGlobalTurnOnSync( - entrypoint = FenixFxAEntryPoint.ShareMenu, + every { sendTabUseCases.sendToAllAsync(any<List<TabData>>()) } returns CompletableDeferred( + true, + ) + every { navController.currentDestination?.id } returns R.id.shareFragment + + val devicesToShareTo = listOf( + Device( + "deviceId0", + "deviceName0", + DeviceType.UNKNOWN, + false, + 0L, + emptyList(), + false, + null, + ), + Device( + "deviceId1", + "deviceName1", + DeviceType.UNKNOWN, + true, + 1L, + emptyList(), + false, + null, ), - null, ) - dismiss(ShareController.Result.DISMISSED) + val tabsShared = slot<List<TabData>>() + + controller.handleShareToAllDevices(devicesToShareTo) + testDispatcher.scheduler.advanceUntilIdle() + + verifyOrder { + sendTabUseCases.sendToAllAsync(capture(tabsShared)) + dismiss(ShareController.Result.SUCCESS) + } + + // SendTabUseCases should send a the `shareTabs` mapped to tabData + assertTrue(tabsShared.isCaptured) + assertEquals(tabsData, tabsShared.captured) } - } @Test - fun `handleReauth should navigate to the Account Problem Fragment and dismiss this one`() { - controller.handleReauth() + fun `handleSignIn should navigate to the Sync Fragment and dismiss this one`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - verifyOrder { - navController.navigate( - ShareFragmentDirections.actionGlobalAccountProblemFragment( - entrypoint = FenixFxAEntryPoint.ShareMenu, - ), - null, + controller.handleSignIn() + + assertNotNull(SyncAccount.signInToSendTab.testGetValue()) + assertEquals(1, SyncAccount.signInToSendTab.testGetValue()!!.size) + assertNull(SyncAccount.signInToSendTab.testGetValue()!!.single().extra) + + verifyOrder { + navController.navigate( + ShareFragmentDirections.actionGlobalTurnOnSync( + entrypoint = FenixFxAEntryPoint.ShareMenu, + ), + null, + ) + dismiss(ShareController.Result.DISMISSED) + } + } + + @Test + fun `handleReauth should navigate to the Account Problem Fragment and dismiss this one`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, ) - dismiss(ShareController.Result.DISMISSED) + + controller.handleReauth() + + verifyOrder { + navController.navigate( + ShareFragmentDirections.actionGlobalAccountProblemFragment( + entrypoint = FenixFxAEntryPoint.ShareMenu, + ), + null, + ) + dismiss(ShareController.Result.DISMISSED) + } } - } @Test - fun `showSuccess should update AppStore with a success action`() { + fun `showSuccess should update AppStore with a success action`() = runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + val destinations = listOf("a", "b") val expectedTabsShared = with(controller) { shareData.toTabData() } controller.showSuccess(destinations) - verify { appStore.dispatch(ShareAction.SharedTabsSuccessfully(destinations, expectedTabsShared)) } + verify { + appStore.dispatch( + ShareAction.SharedTabsSuccessfully( + destinations, + expectedTabsShared, + ), + ) + } } @Test - fun `showFailureWithRetryOption should update AppStore with a failure action`() { - val destinations = listOf("a", "b") - val expectedTabsShared = with(controller) { shareData.toTabData() } + fun `showFailureWithRetryOption should update AppStore with a failure action`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - controller.showFailureWithRetryOption(destinations) + val destinations = listOf("a", "b") + val expectedTabsShared = with(controller) { shareData.toTabData() } - verify { appStore.dispatch(ShareAction.ShareTabsFailed(destinations, expectedTabsShared)) } - } + controller.showFailureWithRetryOption(destinations) + + verify { + appStore.dispatch( + ShareAction.ShareTabsFailed( + destinations, + expectedTabsShared, + ), + ) + } + } @Test - fun `getShareText should respect concatenate shared tabs urls`() { + fun `getShareText should respect concatenate shared tabs urls`() = runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + assertEquals(textToShare, controller.getShareText()) } @Test - fun `getShareText attempts to use original URL for reader pages`() { + fun `getShareText attempts to use original URL for reader pages`() = runTest(testDispatcher) { val shareData = listOf( ShareData(url = "moz-extension://eb8df45a-895b-4f3a-896a-c0c71ae4/page.html"), ShareData(url = "moz-extension://eb8df45a-895b-4f3a-896a-c0c71ae5/page.html?url=url0"), ShareData(url = "url1"), ) val controller = DefaultShareController( - context = context, + context = testContext, appStore = appStore, shareSubject = shareSubject, shareData = shareData, @@ -610,8 +840,8 @@ class ShareControllerTest { sentFromFirefoxManager = sentFromFirefoxManager, navController = navController, recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, dismiss = dismiss, ) @@ -620,33 +850,71 @@ class ShareControllerTest { } @Test - fun `getShareSubject will return 'shareSubject' if that is non null`() { - assertEquals(shareSubject, controller.getShareSubject()) - } + fun `getShareSubject will return 'shareSubject' if that is non null`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + + assertEquals(shareSubject, controller.getShareSubject()) + } @Test - fun `getShareSubject will return a concatenation of tab titles if 'shareSubject' is null`() { - val controller = DefaultShareController( - context = context, - appStore = appStore, - shareSubject = null, - shareData = shareData, - sendTabUseCases = sendTabUseCases, - saveToPdfUseCase = mockk(), - printUseCase = mockk(), - sentFromFirefoxManager = sentFromFirefoxManager, - navController = navController, - recentAppsStorage = recentAppStorage, - viewLifecycleScope = testCoroutineScope, - dispatcher = testDispatcher, - dismiss = dismiss, - ) + fun `getShareSubject will return a concatenation of tab titles if 'shareSubject' is null`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + context = testContext, + appStore = appStore, + shareSubject = null, + shareData = shareData, + sendTabUseCases = sendTabUseCases, + saveToPdfUseCase = mockk(), + printUseCase = mockk(), + sentFromFirefoxManager = sentFromFirefoxManager, + navController = navController, + recentAppsStorage = recentAppStorage, + viewLifecycleScope = this, + ioDispatcher = testDispatcher, + dismiss = dismiss, + ) - assertEquals("title0, title1", controller.getShareSubject()) - } + assertEquals("title0, title1", controller.getShareSubject()) + } @Test - fun `ShareTab#toTabData maps a list of ShareTab to a TabData list`() { + fun `ShareTab#toTabData maps a list of ShareTab to a TabData list`() = runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) + var tabData: List<TabData> with(controller) { @@ -657,20 +925,39 @@ class ShareControllerTest { } @Test - fun `ShareTab#toTabData creates a data url from text if no url is specified`() { - var tabData: List<TabData> - val expected = listOf( - TabData(title = "title0", url = ""), - TabData(title = "title1", url = "data:,Hello%2C%20World!"), - ) + fun `ShareTab#toTabData creates a data url from text if no url is specified`() = + runTest(testDispatcher) { + val controller = DefaultShareController( + testContext, + appStore, + shareSubject, + shareData, + sendTabUseCases, + saveToPdfUseCase, + printUseCase, + sentFromFirefoxManager, + navController, + recentAppStorage, + this, + testDispatcher, + testDispatcher, + FenixFxAEntryPoint.ShareMenu, + dismiss, + ) - with(controller) { - tabData = listOf( - ShareData(title = "title0"), - ShareData(title = "title1", text = "Hello, World!"), - ).toTabData() - } + var tabData: List<TabData> + val expected = listOf( + TabData(title = "title0", url = ""), + TabData(title = "title1", url = "data:,Hello%2C%20World!"), + ) - assertEquals(expected, tabData) - } + with(controller) { + tabData = listOf( + ShareData(title = "title0"), + ShareData(title = "title1", text = "Hello, World!"), + ).toTabData() + } + + assertEquals(expected, tabData) + } }