You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt

375 lines
14 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.share
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.navigation.NavController
import com.google.android.material.snackbar.Snackbar
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.TabData
import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.share.RecentAppsStorage
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.share.listadapters.AppShareOption
@RunWith(FenixRobolectricTestRunner::class)
@ExperimentalCoroutinesApi
class ShareControllerTest {
@MockK(relaxed = true) private lateinit var metrics: MetricController
@MockK(relaxed = true) private lateinit var sendTabUseCases: SendTabUseCases
@MockK(relaxed = true) private lateinit var snackbar: FenixSnackbar
@MockK(relaxed = true) private lateinit var navController: NavController
@MockK(relaxed = true) private lateinit var dismiss: (ShareController.Result) -> Unit
@MockK private lateinit var recentAppStorage: RecentAppsStorage
private val context: Context = testContext
private val shareSubject = "shareSubject"
private val shareData = listOf(
ShareData(url = "url0", title = "title0"),
ShareData(url = "url1", title = "title1")
)
// Navigation between app fragments uses ShareTab as arguments. SendTabUseCases uses TabData.
private val tabsData = listOf(
TabData("title0", "url0"),
TabData("title1", "url1")
)
private val textToShare = "${shareData[0].url}\n\n${shareData[1].url}"
private val testCoroutineScope = TestCoroutineScope()
@Before
fun setUp() {
MockKAnnotations.init(this)
}
@Test
fun `handleShareClosed should call a passed in delegate to close this`() {
val controller = buildController()
controller.handleShareClosed()
verify { dismiss(ShareController.Result.DISMISSED) }
}
@Test
fun `handleShareToApp should start a new sharing activity and close this`() = runBlocking {
val appShareOption = AppShareOption(
name = "app",
icon = mockk(),
packageName = "package",
activityName = "activity"
)
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> {
every { startActivity(capture(shareIntent)) } just Runs
}
val testController = buildController(context = activityContext)
every { recentAppStorage.updateRecentApp(appShareOption.activityName) } just Runs
testController.handleShareToApp(appShareOption)
// Check that the Intent used for querying apps has the expected structure
assertTrue(shareIntent.isCaptured)
assertEquals(Intent.ACTION_SEND, shareIntent.captured.action)
assertEquals(shareSubject, shareIntent.captured.extras!![Intent.EXTRA_SUBJECT])
assertEquals(textToShare, shareIntent.captured.extras!![Intent.EXTRA_TEXT])
assertEquals("text/plain", shareIntent.captured.type)
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, shareIntent.captured.flags)
assertEquals("package", shareIntent.captured.component!!.packageName)
assertEquals("activity", shareIntent.captured.component!!.className)
verify { recentAppStorage.updateRecentApp(appShareOption.activityName) }
verifyOrder {
activityContext.startActivity(shareIntent.captured)
dismiss(ShareController.Result.SUCCESS)
}
}
@Test
fun `handleShareToApp should dismiss with an error start when a security exception occurs`() {
val appShareOption = AppShareOption(
name = "app",
icon = mockk(),
packageName = "package",
activityName = "activity"
)
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> {
every { startActivity(capture(shareIntent)) } throws SecurityException()
every { getString(R.string.share_error_snackbar) } returns "Cannot share to this app"
}
val testController = buildController(context = activityContext)
testController.handleShareToApp(appShareOption)
verifyOrder {
activityContext.startActivity(shareIntent.captured)
snackbar.setText("Cannot share to this app")
snackbar.show()
dismiss(ShareController.Result.SHARE_ERROR)
}
}
@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(activityContext, shareSubject, shareData, mockk(),
snackbar, mockk(), mockk(), testCoroutineScope, dismiss)
every { activityContext.startActivity(capture(shareIntent)) } throws ActivityNotFoundException()
every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app"
testController.handleShareToApp(appShareOption)
verifyOrder {
activityContext.startActivity(shareIntent.captured)
snackbar.setText("Cannot share to this app")
snackbar.show()
dismiss(ShareController.Result.SHARE_ERROR)
}
}
@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>>()
val controller = buildController()
controller.handleShareToDevice(deviceToShareTo)
// Verify all the needed methods are called.
verifyOrder {
metrics.track(Event.SendTab)
sendTabUseCases.sendToDeviceAsync(capture(deviceId), capture(tabsShared))
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
}
assertTrue(deviceId.isCaptured)
assertEquals(deviceToShareTo.id, deviceId.captured)
assertTrue(tabsShared.isCaptured)
assertEquals(tabsData, tabsShared.captured)
}
@Test
@Suppress("DeferredResultUnused")
fun `handleShareToAllDevices calls handleShareToDevice multiple times`() {
val devicesToShareTo = listOf(
Device("deviceId0", "deviceName0", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null),
Device("deviceId1", "deviceName1", DeviceType.UNKNOWN, true, 1L, emptyList(), false, null)
)
val tabsShared = slot<List<TabData>>()
val controller = buildController()
controller.handleShareToAllDevices(devicesToShareTo)
verifyOrder {
sendTabUseCases.sendToAllAsync(capture(tabsShared))
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
}
// 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`() {
val controller = buildController()
controller.handleSignIn()
verifyOrder {
metrics.track(Event.SignInToSendTab)
navController.nav(
R.id.shareFragment,
ShareFragmentDirections.actionGlobalTurnOnSync()
)
dismiss(ShareController.Result.DISMISSED)
}
}
@Test
fun `handleReauth should navigate to the Account Problem Fragment and dismiss this one`() {
val controller = buildController()
controller.handleReauth()
verifyOrder {
navController.nav(
R.id.shareFragment,
ShareFragmentDirections.actionGlobalAccountProblemFragment()
)
dismiss(ShareController.Result.DISMISSED)
}
}
@Test
fun `showSuccess should show a snackbar with a success message`() {
val controller = buildController()
val expectedMessage = controller.getSuccessMessage()
val expectedTimeout = Snackbar.LENGTH_SHORT
controller.showSuccess()
verify {
snackbar.setText(expectedMessage)
snackbar.setLength(expectedTimeout)
}
}
@Test
fun `showFailureWithRetryOption should show a snackbar with a retry action`() {
val expectedMessage = context.getString(R.string.sync_sent_tab_error_snackbar)
val expectedTimeout = Snackbar.LENGTH_LONG
val operation: () -> Unit = { println("Hello World") }
val expectedRetryMessage =
context.getString(R.string.sync_sent_tab_error_snackbar_action)
buildController().showFailureWithRetryOption(operation)
verify {
snackbar.apply {
setText(expectedMessage)
setLength(expectedTimeout)
setAction(expectedRetryMessage, operation)
setAppropriateBackground(true)
}
}
}
@Test
fun `getSuccessMessage should return different strings depending on the number of shared tabs`() {
val controllerWithOneSharedTab = buildController(
data = listOf(ShareData(url = "url0", title = "title0"))
)
val controllerWithMoreSharedTabs = buildController(
data = shareData
)
val expectedTabSharedMessage = context.getString(R.string.sync_sent_tab_snackbar)
val expectedTabsSharedMessage = context.getString(R.string.sync_sent_tabs_snackbar)
val tabSharedMessage = controllerWithOneSharedTab.getSuccessMessage()
val tabsSharedMessage = controllerWithMoreSharedTabs.getSuccessMessage()
assertNotEquals(tabsSharedMessage, tabSharedMessage)
assertEquals(expectedTabSharedMessage, tabSharedMessage)
assertEquals(expectedTabsSharedMessage, tabsSharedMessage)
}
@Test
fun `getShareText should respect concatenate shared tabs urls`() {
assertEquals(textToShare, buildController(data = shareData).getShareText())
}
@Test
fun `getShareText attempts to use original URL for reader pages`() {
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 = buildController(data = shareData)
val expectedShareText = "${shareData[0].url}\n\nurl0\n\n${shareData[2].url}"
assertEquals(expectedShareText, controller.getShareText())
}
@Test
fun `getShareSubject will return "shareSubject" if that is non null`() {
assertEquals(shareSubject, buildController(subject = shareSubject).getShareSubject())
}
@Test
fun `getShareSubject will return a concatenation of tab titles if "shareSubject" is null`() {
val controller = buildController(subject = null)
assertEquals("title0, title1", controller.getShareSubject())
}
@Test
fun `ShareTab#toTabData maps a list of ShareTab to a TabData list`() {
val tabData = with(buildController()) {
shareData.toTabData()
}
assertEquals(tabsData, tabData)
}
@Test
fun `ShareTab#toTabData creates a data url from text if no url is specified`() {
val expected = listOf(
TabData(title = "title0", url = ""),
TabData(title = "title1", url = "data:,Hello%2C%20World!")
)
val tabData = with(buildController()) {
listOf(
ShareData(title = "title0"),
ShareData(title = "title1", text = "Hello, World!")
).toTabData()
}
assertEquals(expected, tabData)
}
private fun buildController(
context: Context = this.context,
subject: String? = shareSubject,
data: List<ShareData> = shareData
) = DefaultShareController(
context = context,
metrics = metrics,
shareSubject = subject,
shareData = data,
sendTabUseCases = sendTabUseCases,
snackbar = snackbar,
navController = navController,
recentAppsStorage = recentAppStorage,
viewLifecycleScope = testCoroutineScope,
dismiss = dismiss
)
}