2019-08-29 17:47:49 +02:00
|
|
|
/* 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
|
|
|
|
|
2019-09-26 21:30:28 +02:00
|
|
|
import android.app.Activity
|
2019-09-04 17:46:34 +02:00
|
|
|
import android.content.Context
|
2019-08-29 17:47:49 +02:00
|
|
|
import android.content.Intent
|
|
|
|
import androidx.navigation.NavController
|
|
|
|
import assertk.assertAll
|
|
|
|
import assertk.assertThat
|
|
|
|
import assertk.assertions.isEqualTo
|
2019-09-26 21:30:28 +02:00
|
|
|
import assertk.assertions.isNotEqualTo
|
|
|
|
import assertk.assertions.isSameAs
|
|
|
|
import assertk.assertions.isSuccess
|
2019-08-29 17:47:49 +02:00
|
|
|
import assertk.assertions.isTrue
|
2019-09-26 21:30:28 +02:00
|
|
|
import com.google.android.material.snackbar.Snackbar
|
2019-08-29 17:47:49 +02:00
|
|
|
import io.mockk.Runs
|
|
|
|
import io.mockk.every
|
|
|
|
import io.mockk.just
|
|
|
|
import io.mockk.mockk
|
|
|
|
import io.mockk.slot
|
2019-09-26 21:30:28 +02:00
|
|
|
import io.mockk.spyk
|
2019-08-29 17:47:49 +02:00
|
|
|
import io.mockk.verify
|
|
|
|
import io.mockk.verifyOrder
|
2019-11-25 20:07:21 +01:00
|
|
|
import mozilla.components.concept.engine.prompt.ShareData
|
2019-08-29 17:47:49 +02:00
|
|
|
import mozilla.components.concept.sync.Device
|
|
|
|
import mozilla.components.concept.sync.DeviceType
|
2019-09-02 16:24:36 +02:00
|
|
|
import mozilla.components.concept.sync.TabData
|
|
|
|
import mozilla.components.feature.sendtab.SendTabUseCases
|
2019-09-26 21:30:28 +02:00
|
|
|
import mozilla.components.support.test.robolectric.testContext
|
2019-09-04 17:46:34 +02:00
|
|
|
import org.junit.Before
|
2019-08-29 17:47:49 +02:00
|
|
|
import org.junit.Test
|
|
|
|
import org.junit.runner.RunWith
|
|
|
|
import org.mozilla.fenix.R
|
|
|
|
import org.mozilla.fenix.TestApplication
|
2019-09-26 21:30:28 +02:00
|
|
|
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
2019-09-04 17:46:34 +02:00
|
|
|
import org.mozilla.fenix.components.metrics.Event
|
|
|
|
import org.mozilla.fenix.components.metrics.MetricController
|
|
|
|
import org.mozilla.fenix.ext.metrics
|
2019-08-29 17:47:49 +02:00
|
|
|
import org.mozilla.fenix.ext.nav
|
2019-11-06 02:30:04 +01:00
|
|
|
import org.mozilla.fenix.share.listadapters.AppShareOption
|
2019-08-29 17:47:49 +02:00
|
|
|
import org.robolectric.RobolectricTestRunner
|
|
|
|
import org.robolectric.annotation.Config
|
|
|
|
|
|
|
|
@RunWith(RobolectricTestRunner::class)
|
|
|
|
@Config(application = TestApplication::class)
|
|
|
|
class ShareControllerTest {
|
2019-09-26 21:30:28 +02:00
|
|
|
// Need a valid context to retrieve Strings for example, but we also need it to return our "metrics"
|
|
|
|
private val context: Context = spyk(testContext)
|
2019-09-04 17:46:34 +02:00
|
|
|
private val metrics: MetricController = mockk(relaxed = true)
|
2019-11-25 20:07:21 +01:00
|
|
|
private val shareData = listOf(
|
|
|
|
ShareData(url = "url0", title = "title0"),
|
|
|
|
ShareData(url = "url1", title = "title1")
|
2019-08-29 17:47:49 +02:00
|
|
|
)
|
2019-09-02 16:24:36 +02:00
|
|
|
// Navigation between app fragments uses ShareTab as arguments. SendTabUseCases uses TabData.
|
|
|
|
private val tabsData = listOf(
|
|
|
|
TabData("title0", "url0"),
|
|
|
|
TabData("title1", "url1")
|
|
|
|
)
|
2019-11-25 20:07:21 +01:00
|
|
|
private val textToShare = "${shareData[0].url}\n${shareData[1].url}"
|
2019-09-02 16:24:36 +02:00
|
|
|
private val sendTabUseCases = mockk<SendTabUseCases>(relaxed = true)
|
2019-09-26 21:30:28 +02:00
|
|
|
private val snackbarPresenter = mockk<FenixSnackbarPresenter>(relaxed = true)
|
2019-08-29 17:47:49 +02:00
|
|
|
private val navController = mockk<NavController>(relaxed = true)
|
2019-12-10 19:57:06 +01:00
|
|
|
private val dismiss = mockk<(ShareController.Result) -> Unit>(relaxed = true)
|
2019-09-26 21:30:28 +02:00
|
|
|
private val controller = DefaultShareController(
|
2019-11-25 20:07:21 +01:00
|
|
|
context, shareData, sendTabUseCases, snackbarPresenter, navController, dismiss
|
2019-09-26 21:30:28 +02:00
|
|
|
)
|
2019-09-04 17:46:34 +02:00
|
|
|
|
|
|
|
@Before
|
|
|
|
fun setUp() {
|
|
|
|
every { context.metrics } returns metrics
|
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
|
|
|
|
@Test
|
|
|
|
fun `handleShareClosed should call a passed in delegate to close this`() {
|
|
|
|
controller.handleShareClosed()
|
|
|
|
|
2019-12-10 19:57:06 +01:00
|
|
|
verify { dismiss(ShareController.Result.DISMISSED) }
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun `handleShareToApp should start a new sharing activity and close this`() {
|
|
|
|
val appPackageName = "package"
|
|
|
|
val appClassName = "activity"
|
2019-11-06 02:30:04 +01:00
|
|
|
val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName)
|
2019-08-29 17:47:49 +02:00
|
|
|
val shareIntent = slot<Intent>()
|
2019-09-26 21:30:28 +02:00
|
|
|
// 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>()
|
2019-11-25 20:07:21 +01:00
|
|
|
val testController = DefaultShareController(activityContext, shareData, mockk(), mockk(), mockk(), dismiss)
|
2019-09-26 21:30:28 +02:00
|
|
|
every { activityContext.startActivity(capture(shareIntent)) } just Runs
|
2019-08-29 17:47:49 +02:00
|
|
|
|
2019-09-26 21:30:28 +02:00
|
|
|
testController.handleShareToApp(appShareOption)
|
2019-08-29 17:47:49 +02:00
|
|
|
|
2019-12-10 19:57:06 +01:00
|
|
|
// Check that the Intent used for querying apps has the expected structure
|
2019-08-29 17:47:49 +02:00
|
|
|
assertAll {
|
|
|
|
assertThat(shareIntent.isCaptured).isTrue()
|
|
|
|
assertThat(shareIntent.captured.action).isEqualTo(Intent.ACTION_SEND)
|
|
|
|
assertThat(shareIntent.captured.extras!![Intent.EXTRA_TEXT]).isEqualTo(textToShare)
|
|
|
|
assertThat(shareIntent.captured.type).isEqualTo("text/plain")
|
|
|
|
assertThat(shareIntent.captured.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
|
|
assertThat(shareIntent.captured.component!!.packageName).isEqualTo(appPackageName)
|
|
|
|
assertThat(shareIntent.captured.component!!.className).isEqualTo(appClassName)
|
|
|
|
}
|
|
|
|
verifyOrder {
|
2019-09-26 21:30:28 +02:00
|
|
|
activityContext.startActivity(shareIntent.captured)
|
2019-12-10 19:57:06 +01:00
|
|
|
dismiss(ShareController.Result.SUCCESS)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@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(activityContext, shareData, mockk(), snackbarPresenter, mockk(), dismiss)
|
|
|
|
every { activityContext.startActivity(capture(shareIntent)) } throws SecurityException()
|
|
|
|
every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app"
|
|
|
|
|
|
|
|
testController.handleShareToApp(appShareOption)
|
|
|
|
|
|
|
|
verifyOrder {
|
|
|
|
activityContext.startActivity(shareIntent.captured)
|
|
|
|
snackbarPresenter.present("Cannot share to this app")
|
|
|
|
dismiss(ShareController.Result.SHARE_ERROR)
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
@Suppress("DeferredResultUnused")
|
|
|
|
fun `handleShareToDevice should share to account device, inform callbacks and dismiss`() {
|
2019-09-26 21:30:28 +02:00
|
|
|
val deviceToShareTo = Device(
|
|
|
|
"deviceId", "deviceName", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null)
|
2019-08-29 17:47:49 +02:00
|
|
|
val deviceId = slot<String>()
|
2019-09-02 16:24:36 +02:00
|
|
|
val tabsShared = slot<List<TabData>>()
|
2019-08-29 17:47:49 +02:00
|
|
|
|
|
|
|
controller.handleShareToDevice(deviceToShareTo)
|
|
|
|
|
|
|
|
// Verify all the needed methods are called.
|
2019-09-02 16:24:36 +02:00
|
|
|
verifyOrder {
|
2019-09-04 17:46:34 +02:00
|
|
|
metrics.track(Event.SendTab)
|
2019-09-02 16:24:36 +02:00
|
|
|
sendTabUseCases.sendToDeviceAsync(capture(deviceId), capture(tabsShared))
|
2019-09-26 21:30:28 +02:00
|
|
|
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
|
2019-09-02 16:24:36 +02:00
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
assertAll {
|
|
|
|
assertThat(deviceId.isCaptured).isTrue()
|
|
|
|
assertThat(deviceId.captured).isEqualTo(deviceToShareTo.id)
|
2019-09-02 16:24:36 +02:00
|
|
|
assertThat(tabsShared.isCaptured).isTrue()
|
|
|
|
assertThat(tabsShared.captured).isEqualTo(tabsData)
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
2019-09-26 21:30:28 +02:00
|
|
|
@Suppress("DeferredResultUnused")
|
2019-08-29 17:47:49 +02:00
|
|
|
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)
|
|
|
|
)
|
2019-09-02 16:24:36 +02:00
|
|
|
val tabsShared = slot<List<TabData>>()
|
2019-08-29 17:47:49 +02:00
|
|
|
|
|
|
|
controller.handleShareToAllDevices(devicesToShareTo)
|
|
|
|
|
2019-09-02 16:24:36 +02:00
|
|
|
verifyOrder {
|
|
|
|
sendTabUseCases.sendToAllAsync(capture(tabsShared))
|
2019-09-26 21:30:28 +02:00
|
|
|
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
|
2019-09-02 16:24:36 +02:00
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
assertAll {
|
2019-09-02 16:24:36 +02:00
|
|
|
// SendTabUseCases should send a the `shareTabs` mapped to tabData
|
|
|
|
assertThat(tabsShared.isCaptured).isTrue()
|
|
|
|
assertThat(tabsShared.captured).isEqualTo(tabsData)
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun `handleSignIn should navigate to the Sync Fragment and dismiss this one`() {
|
|
|
|
controller.handleSignIn()
|
|
|
|
|
|
|
|
verifyOrder {
|
2019-09-04 17:46:34 +02:00
|
|
|
metrics.track(Event.SignInToSendTab)
|
2019-08-29 17:47:49 +02:00
|
|
|
navController.nav(
|
|
|
|
R.id.shareFragment,
|
|
|
|
ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment()
|
|
|
|
)
|
2019-12-10 19:57:06 +01:00
|
|
|
dismiss(ShareController.Result.DISMISSED)
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-05 20:13:03 +02:00
|
|
|
@Test
|
|
|
|
fun `handleReauth should navigate to the Account Problem Fragment and dismiss this one`() {
|
|
|
|
controller.handleReauth()
|
|
|
|
|
|
|
|
verifyOrder {
|
|
|
|
navController.nav(
|
|
|
|
R.id.shareFragment,
|
|
|
|
ShareFragmentDirections.actionShareFragmentToAccountProblemFragment()
|
|
|
|
)
|
2019-12-10 19:57:06 +01:00
|
|
|
dismiss(ShareController.Result.DISMISSED)
|
2019-09-05 20:13:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-26 21:30:28 +02:00
|
|
|
@Test
|
|
|
|
fun `showSuccess should show a snackbar with a success message`() {
|
|
|
|
val expectedMessage = controller.getSuccessMessage()
|
|
|
|
val expectedTimeout = Snackbar.LENGTH_SHORT
|
|
|
|
val messageSlot = slot<String>()
|
|
|
|
val timeoutSlot = slot<Int>()
|
|
|
|
|
|
|
|
controller.showSuccess()
|
|
|
|
|
|
|
|
verify { snackbarPresenter.present(capture(messageSlot), capture(timeoutSlot)) }
|
|
|
|
assertAll {
|
|
|
|
assertThat(messageSlot.isCaptured).isTrue()
|
|
|
|
assertThat(timeoutSlot.isCaptured).isTrue()
|
|
|
|
|
|
|
|
assertThat(messageSlot.captured).isEqualTo(expectedMessage)
|
|
|
|
assertThat(timeoutSlot.captured).isEqualTo(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)
|
|
|
|
val messageSlot = slot<String>()
|
|
|
|
val timeoutSlot = slot<Int>()
|
|
|
|
val operationSlot = slot<() -> Unit>()
|
|
|
|
val retryMesageSlot = slot<String>()
|
|
|
|
val isFailureSlot = slot<Boolean>()
|
|
|
|
|
|
|
|
controller.showFailureWithRetryOption(operation)
|
|
|
|
|
|
|
|
verify {
|
|
|
|
snackbarPresenter.present(
|
|
|
|
capture(messageSlot),
|
|
|
|
capture(timeoutSlot),
|
|
|
|
capture(operationSlot),
|
|
|
|
capture(retryMesageSlot),
|
|
|
|
capture(isFailureSlot)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
assertAll {
|
|
|
|
assertThat(messageSlot.isCaptured).isTrue()
|
|
|
|
assertThat(timeoutSlot.isCaptured).isTrue()
|
|
|
|
assertThat(operationSlot.isCaptured).isTrue()
|
|
|
|
assertThat(retryMesageSlot.isCaptured).isTrue()
|
|
|
|
assertThat(isFailureSlot.isCaptured).isTrue()
|
|
|
|
|
|
|
|
assertThat(messageSlot.captured).isEqualTo(expectedMessage)
|
|
|
|
assertThat(timeoutSlot.captured).isEqualTo(expectedTimeout)
|
|
|
|
assertThat { operationSlot.captured }.isSuccess().isSameAs(operation)
|
|
|
|
assertThat(retryMesageSlot.captured).isEqualTo(expectedRetryMessage)
|
|
|
|
assertThat(isFailureSlot.captured).isEqualTo(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun `getSuccessMessage should return different strings depending on the number of shared tabs`() {
|
|
|
|
val controllerWithOneSharedTab = DefaultShareController(
|
2019-11-25 20:07:21 +01:00
|
|
|
context, listOf(ShareData(url = "url0", title = "title0")), mockk(), mockk(), mockk(), mockk()
|
2019-09-26 21:30:28 +02:00
|
|
|
)
|
|
|
|
val controllerWithMoreSharedTabs = controller
|
|
|
|
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()
|
|
|
|
|
|
|
|
assertAll {
|
|
|
|
assertThat(tabSharedMessage).isNotEqualTo(tabsSharedMessage)
|
|
|
|
assertThat(tabSharedMessage).isEqualTo(expectedTabSharedMessage)
|
|
|
|
assertThat(tabsSharedMessage).isEqualTo(expectedTabsSharedMessage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-29 17:47:49 +02:00
|
|
|
@Test
|
2019-09-02 16:24:36 +02:00
|
|
|
fun `getShareText should respect concatenate shared tabs urls`() {
|
|
|
|
assertThat(controller.getShareText()).isEqualTo(textToShare)
|
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
|
|
|
|
@Test
|
2019-09-02 16:24:36 +02:00
|
|
|
fun `ShareTab#toTabData maps a list of ShareTab to a TabData list`() {
|
|
|
|
var tabData: List<TabData>
|
|
|
|
|
|
|
|
with(controller) {
|
2019-11-25 20:07:21 +01:00
|
|
|
tabData = shareData.toTabData()
|
2019-09-02 16:24:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
assertThat(tabData).isEqualTo(tabsData)
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|
2019-12-10 19:57:06 +01:00
|
|
|
|
|
|
|
@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!")
|
|
|
|
)
|
|
|
|
|
|
|
|
with(controller) {
|
|
|
|
tabData = listOf(
|
|
|
|
ShareData(title = "title0"),
|
|
|
|
ShareData(title = "title1", text = "Hello, World!")
|
|
|
|
).toTabData()
|
|
|
|
}
|
|
|
|
|
|
|
|
assertThat(tabData).isEqualTo(expected)
|
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|