@ -4,20 +4,25 @@
package org.mozilla.fenix.share
package org.mozilla.fenix.share
import android.app.Activity
import android.content.Context
import android.content.Context
import android.content.Intent
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavController
import assertk.assertAll
import assertk.assertAll
import assertk.assertThat
import assertk.assertThat
import assertk.assertions.isDataClassEqualTo
import assertk.assertions.isDataClassEqualTo
import assertk.assertions.isEqualTo
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import assertk.assertions.isSameAs
import assertk.assertions.isSuccess
import assertk.assertions.isTrue
import assertk.assertions.isTrue
import com.google.android.material.snackbar.Snackbar
import io.mockk.Runs
import io.mockk.Runs
import io.mockk.every
import io.mockk.every
import io.mockk.just
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockk
import io.mockk.slot
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import io.mockk.verify
import io.mockk.verifyOrder
import io.mockk.verifyOrder
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
@ -25,12 +30,13 @@ import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.TabData
import mozilla.components.concept.sync.TabData
import mozilla.components.feature.sendtab.SendTabUseCases
import mozilla.components.feature.sendtab.SendTabUseCases
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Before
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.R
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.components.FenixSnackbarPresenter
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.metrics
@ -43,9 +49,9 @@ import org.robolectric.annotation.Config
@RunWith ( RobolectricTestRunner :: class )
@RunWith ( RobolectricTestRunner :: class )
@Config ( application = TestApplication :: class )
@Config ( application = TestApplication :: class )
class ShareControllerTest {
class ShareControllerTest {
private val context : Context = mockk ( relaxed = true )
// 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 metrics : MetricController = mockk ( relaxed = true )
private val metrics : MetricController = mockk ( relaxed = true )
private val fragment = mockk < Fragment > ( relaxed = true )
private val shareTabs = listOf (
private val shareTabs = listOf (
ShareTab ( " url0 " , " title0 " ) ,
ShareTab ( " url0 " , " title0 " ) ,
ShareTab ( " url1 " , " title1 " )
ShareTab ( " url1 " , " title1 " )
@ -57,9 +63,12 @@ class ShareControllerTest {
)
)
private val textToShare = " ${shareTabs[0].url} \n ${shareTabs[1].url} "
private val textToShare = " ${shareTabs[0].url} \n ${shareTabs[1].url} "
private val sendTabUseCases = mockk < SendTabUseCases > ( relaxed = true )
private val sendTabUseCases = mockk < SendTabUseCases > ( relaxed = true )
private val snackbarPresenter = mockk < FenixSnackbarPresenter > ( relaxed = true )
private val navController = mockk < NavController > ( relaxed = true )
private val navController = mockk < NavController > ( relaxed = true )
private val dismiss = mockk < ( ) -> Unit > ( relaxed = true )
private val dismiss = mockk < ( ) -> Unit > ( relaxed = true )
private val controller = DefaultShareController ( context , fragment , shareTabs , sendTabUseCases , navController , dismiss )
private val controller = DefaultShareController (
context , shareTabs , sendTabUseCases , snackbarPresenter , navController , dismiss
)
@Before
@Before
fun setUp ( ) {
fun setUp ( ) {
@ -79,9 +88,14 @@ class ShareControllerTest {
val appClassName = " activity "
val appClassName = " activity "
val appShareOption = AppShareOption ( " app " , mockk ( ) , appPackageName , appClassName )
val appShareOption = AppShareOption ( " app " , mockk ( ) , appPackageName , appClassName )
val shareIntent = slot < Intent > ( )
val shareIntent = slot < Intent > ( )
every { fragment . startActivity ( capture ( shareIntent ) ) } just Runs
// 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 , shareTabs , mockk ( ) , mockk ( ) , mockk ( ) , dismiss )
every { activityContext . startActivity ( capture ( shareIntent ) ) } just Runs
controller . handleShareToApp ( appShareOption )
testC ontroller. handleShareToApp ( appShareOption )
// Check that the Intent used for querying apps has the expected structre
// Check that the Intent used for querying apps has the expected structre
assertAll {
assertAll {
@ -94,7 +108,7 @@ class ShareControllerTest {
assertThat ( shareIntent . captured . component !! . className ) . isEqualTo ( appClassName )
assertThat ( shareIntent . captured . component !! . className ) . isEqualTo ( appClassName )
}
}
verifyOrder {
verifyOrder {
fragmen t. startActivity ( shareIntent . captured )
activityContex t. startActivity ( shareIntent . captured )
dismiss ( )
dismiss ( )
}
}
}
}
@ -102,13 +116,10 @@ class ShareControllerTest {
@Test
@Test
@Suppress ( " DeferredResultUnused " )
@Suppress ( " DeferredResultUnused " )
fun `handleShareToDevice should share to account device, inform callbacks and dismiss` ( ) {
fun `handleShareToDevice should share to account device, inform callbacks and dismiss` ( ) {
val deviceToShareTo =
val deviceToShareTo = Device (
Device ( " deviceId " , " deviceName " , DeviceType . UNKNOWN , false , 0L , emptyList ( ) , false , null )
" deviceId " , " deviceName " , DeviceType . UNKNOWN , false , 0L , emptyList ( ) , false , null )
val tabSharedCallbackActivity = mockk < HomeActivity > ( relaxed = true )
val sharedTabsNumber = slot < Int > ( )
val deviceId = slot < String > ( )
val deviceId = slot < String > ( )
val tabsShared = slot < List < TabData > > ( )
val tabsShared = slot < List < TabData > > ( )
every { fragment . activity } returns tabSharedCallbackActivity
controller . handleShareToDevice ( deviceToShareTo )
controller . handleShareToDevice ( deviceToShareTo )
@ -116,48 +127,35 @@ class ShareControllerTest {
verifyOrder {
verifyOrder {
metrics . track ( Event . SendTab )
metrics . track ( Event . SendTab )
sendTabUseCases . sendToDeviceAsync ( capture ( deviceId ) , capture ( tabsShared ) )
sendTabUseCases . sendToDeviceAsync ( capture ( deviceId ) , capture ( tabsShared ) )
tabSharedCallbackActivity . onTabsShared ( capture ( sharedTabsNumber ) )
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
dismiss ( )
}
}
assertAll {
assertAll {
assertThat ( deviceId . isCaptured ) . isTrue ( )
assertThat ( deviceId . isCaptured ) . isTrue ( )
assertThat ( deviceId . captured ) . isEqualTo ( deviceToShareTo . id )
assertThat ( deviceId . captured ) . isEqualTo ( deviceToShareTo . id )
assertThat ( tabsShared . isCaptured ) . isTrue ( )
assertThat ( tabsShared . isCaptured ) . isTrue ( )
assertThat ( tabsShared . captured ) . isEqualTo ( tabsData )
assertThat ( tabsShared . captured ) . isEqualTo ( tabsData )
// All current tabs should be shared
assertThat ( sharedTabsNumber . isCaptured ) . isTrue ( )
assertThat ( sharedTabsNumber . captured ) . isEqualTo ( shareTabs . size )
}
}
}
}
@Test
@Test
@Suppress ( " DeferredResultUnused " )
fun `handleShareToAllDevices calls handleShareToDevice multiple times` ( ) {
fun `handleShareToAllDevices calls handleShareToDevice multiple times` ( ) {
val devicesToShareTo = listOf (
val devicesToShareTo = listOf (
Device ( " deviceId0 " , " deviceName0 " , DeviceType . UNKNOWN , false , 0L , emptyList ( ) , false , null ) ,
Device ( " deviceId0 " , " deviceName0 " , DeviceType . UNKNOWN , false , 0L , emptyList ( ) , false , null ) ,
Device ( " deviceId1 " , " deviceName1 " , DeviceType . UNKNOWN , true , 1L , emptyList ( ) , false , null )
Device ( " deviceId1 " , " deviceName1 " , DeviceType . UNKNOWN , true , 1L , emptyList ( ) , false , null )
)
)
val tabSharedCallbackActivity = mockk < HomeActivity > ( relaxed = true )
val sharedTabsNumber = slot < Int > ( )
val tabsShared = slot < List < TabData > > ( )
val tabsShared = slot < List < TabData > > ( )
every { fragment . activity } returns tabSharedCallbackActivity
controller . handleShareToAllDevices ( devicesToShareTo )
controller . handleShareToAllDevices ( devicesToShareTo )
// Verify all the needed methods are called. sendTab() should be called for each account device.
verifyOrder {
verifyOrder {
sendTabUseCases . sendToAllAsync ( capture ( tabsShared ) )
sendTabUseCases . sendToAllAsync ( capture ( tabsShared ) )
tabSharedCallbackActivity . onTabsShared ( capture ( sharedTabsNumber ) )
// dismiss() is also to be called, but at the moment cannot test it in a coroutine.
dismiss ( )
}
}
assertAll {
assertAll {
// SendTabUseCases should send a the `shareTabs` mapped to tabData
// SendTabUseCases should send a the `shareTabs` mapped to tabData
assertThat ( tabsShared . isCaptured ) . isTrue ( )
assertThat ( tabsShared . isCaptured ) . isTrue ( )
assertThat ( tabsShared . captured ) . isEqualTo ( tabsData )
assertThat ( tabsShared . captured ) . isEqualTo ( tabsData )
// All current tabs should be shared
assertThat ( sharedTabsNumber . isCaptured ) . isTrue ( )
assertThat ( sharedTabsNumber . captured ) . isEqualTo ( shareTabs . size )
}
}
}
}
@ -188,6 +186,83 @@ class ShareControllerTest {
}
}
}
}
@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 (
context , listOf ( ShareTab ( " url0 " , " title0 " ) ) , mockk ( ) , mockk ( ) , mockk ( ) , mockk ( )
)
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 )
}
}
@Test
@Test
fun `getShareText should respect concatenate shared tabs urls` ( ) {
fun `getShareText should respect concatenate shared tabs urls` ( ) {
assertThat ( controller . getShareText ( ) ) . isEqualTo ( textToShare )
assertThat ( controller . getShareText ( ) ) . isEqualTo ( textToShare )