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
|
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-29 14:31:08 +01:00
|
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
|
|
import kotlinx.coroutines.runBlocking
|
|
|
|
import kotlinx.coroutines.test.TestCoroutineScope
|
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
|
2019-12-13 20:24:42 +01:00
|
|
|
import mozilla.components.feature.accounts.push.SendTabUseCases
|
2019-11-29 14:31:08 +01:00
|
|
|
import mozilla.components.feature.share.RecentAppsStorage
|
2019-09-26 21:30:28 +02:00
|
|
|
import mozilla.components.support.test.robolectric.testContext
|
2020-04-02 18:52:56 +02:00
|
|
|
import org.junit.Assert.assertEquals
|
|
|
|
import org.junit.Assert.assertNotEquals
|
|
|
|
import org.junit.Assert.assertTrue
|
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
|
2020-01-02 23:31:52 +01:00
|
|
|
import org.mozilla.fenix.components.FenixSnackbar
|
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
|
2020-04-01 23:00:32 +02:00
|
|
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
2020-06-03 18:42:02 +02:00
|
|
|
import org.mozilla.fenix.share.listadapters.AppShareOption
|
2019-08-29 17:47:49 +02:00
|
|
|
|
2020-04-01 23:00:32 +02:00
|
|
|
@RunWith(FenixRobolectricTestRunner::class)
|
2019-11-29 14:31:08 +01:00
|
|
|
@ExperimentalCoroutinesApi
|
2019-08-29 17:47:49 +02:00
|
|
|
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")
|
|
|
|
)
|
2020-06-03 18:42:02 +02:00
|
|
|
private val textToShare = "${shareData[0].url}\n\n${shareData[1].url}"
|
2019-11-29 14:31:08 +01:00
|
|
|
private val testCoroutineScope = TestCoroutineScope()
|
2019-09-02 16:24:36 +02:00
|
|
|
private val sendTabUseCases = mockk<SendTabUseCases>(relaxed = true)
|
2020-01-02 23:31:52 +01:00
|
|
|
private val snackbar = mockk<FenixSnackbar>(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-11-29 14:31:08 +01:00
|
|
|
private val recentAppStorage = mockk<RecentAppsStorage>(relaxed = true)
|
2019-09-26 21:30:28 +02:00
|
|
|
private val controller = DefaultShareController(
|
2019-11-29 14:31:08 +01:00
|
|
|
context, shareData, sendTabUseCases, snackbar, navController,
|
|
|
|
recentAppStorage, testCoroutineScope, 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
|
2019-11-29 14:31:08 +01:00
|
|
|
fun `handleShareToApp should start a new sharing activity and close this`() = runBlocking {
|
2019-08-29 17:47:49 +02:00
|
|
|
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-29 14:31:08 +01:00
|
|
|
val testController = DefaultShareController(activityContext, shareData, mockk(), mockk(), mockk(),
|
|
|
|
recentAppStorage, testCoroutineScope, dismiss)
|
2019-09-26 21:30:28 +02:00
|
|
|
every { activityContext.startActivity(capture(shareIntent)) } just Runs
|
2020-03-24 15:42:07 +01:00
|
|
|
every { recentAppStorage.updateRecentApp(appShareOption.activityName) } 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
|
2020-04-02 18:52:56 +02:00
|
|
|
assertTrue(shareIntent.isCaptured)
|
|
|
|
assertEquals(Intent.ACTION_SEND, shareIntent.captured.action)
|
|
|
|
assertEquals(textToShare, shareIntent.captured.extras!![Intent.EXTRA_TEXT])
|
|
|
|
assertEquals("text/plain", shareIntent.captured.type)
|
|
|
|
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, shareIntent.captured.flags)
|
|
|
|
assertEquals(appPackageName, shareIntent.captured.component!!.packageName)
|
|
|
|
assertEquals(appClassName, shareIntent.captured.component!!.className)
|
|
|
|
|
2020-06-02 21:46:28 +02:00
|
|
|
verify { recentAppStorage.updateRecentApp(appShareOption.activityName) }
|
2019-08-29 17:47:49 +02:00
|
|
|
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>()
|
2019-11-29 14:31:08 +01:00
|
|
|
val testController = DefaultShareController(activityContext, shareData, mockk(), snackbar,
|
|
|
|
mockk(), mockk(), testCoroutineScope, dismiss)
|
2019-12-10 19:57:06 +01:00
|
|
|
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)
|
2020-01-02 23:31:52 +01:00
|
|
|
snackbar.setText("Cannot share to this app")
|
|
|
|
snackbar.show()
|
2019-12-10 19:57:06 +01:00
|
|
|
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
|
|
|
}
|
2020-04-02 18:52:56 +02:00
|
|
|
|
|
|
|
assertTrue(deviceId.isCaptured)
|
|
|
|
assertEquals(deviceToShareTo.id, deviceId.captured)
|
|
|
|
assertTrue(tabsShared.isCaptured)
|
|
|
|
assertEquals(tabsData, tabsShared.captured)
|
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
|
|
|
}
|
2020-04-02 18:52:56 +02:00
|
|
|
|
|
|
|
// SendTabUseCases should send a the `shareTabs` mapped to tabData
|
|
|
|
assertTrue(tabsShared.isCaptured)
|
|
|
|
assertEquals(tabsData, tabsShared.captured)
|
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,
|
2020-04-14 06:43:45 +02:00
|
|
|
ShareFragmentDirections.actionGlobalTurnOnSync()
|
2019-08-29 17:47:49 +02:00
|
|
|
)
|
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,
|
2020-04-14 06:43:45 +02:00
|
|
|
ShareFragmentDirections.actionGlobalAccountProblemFragment()
|
2019-09-05 20:13:03 +02:00
|
|
|
)
|
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
|
|
|
|
|
|
|
|
controller.showSuccess()
|
|
|
|
|
2020-01-02 23:31:52 +01:00
|
|
|
verify {
|
|
|
|
snackbar.setText(expectedMessage)
|
|
|
|
snackbar.setLength(expectedTimeout)
|
2019-09-26 21:30:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
controller.showFailureWithRetryOption(operation)
|
|
|
|
|
|
|
|
verify {
|
2020-01-02 23:31:52 +01:00
|
|
|
snackbar.apply {
|
|
|
|
setText(expectedMessage)
|
|
|
|
setLength(expectedTimeout)
|
|
|
|
setAction(expectedRetryMessage, operation)
|
|
|
|
setAppropriateBackground(true)
|
|
|
|
}
|
2019-09-26 21:30:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun `getSuccessMessage should return different strings depending on the number of shared tabs`() {
|
|
|
|
val controllerWithOneSharedTab = DefaultShareController(
|
2019-11-29 14:31:08 +01:00
|
|
|
context,
|
|
|
|
listOf(ShareData(url = "url0", title = "title0")),
|
|
|
|
mockk(),
|
|
|
|
mockk(),
|
|
|
|
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()
|
|
|
|
|
2020-04-02 18:52:56 +02:00
|
|
|
assertNotEquals(tabsSharedMessage, tabSharedMessage)
|
|
|
|
assertEquals(expectedTabSharedMessage, tabSharedMessage)
|
|
|
|
assertEquals(expectedTabsSharedMessage, tabsSharedMessage)
|
2019-09-26 21:30:28 +02:00
|
|
|
}
|
|
|
|
|
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`() {
|
2020-04-02 18:52:56 +02:00
|
|
|
assertEquals(textToShare, controller.getShareText())
|
2019-09-02 16:24:36 +02:00
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
|
2020-06-25 16:54:52 +02:00
|
|
|
@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 = DefaultShareController(
|
|
|
|
context, shareData, sendTabUseCases, snackbar, navController,
|
|
|
|
recentAppStorage, testCoroutineScope, dismiss
|
|
|
|
)
|
|
|
|
|
|
|
|
val expectedShareText = "${shareData[0].url}\n\nurl0\n\n${shareData[2].url}"
|
|
|
|
assertEquals(expectedShareText, controller.getShareText())
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-04-02 18:52:56 +02:00
|
|
|
assertEquals(tabsData, tabData)
|
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()
|
|
|
|
}
|
|
|
|
|
2020-04-02 18:52:56 +02:00
|
|
|
assertEquals(expected, tabData)
|
2019-12-10 19:57:06 +01:00
|
|
|
}
|
2019-08-29 17:47:49 +02:00
|
|
|
}
|