2022-03-28 20:13:07 +00: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.home.recentsyncedtabs
2022-04-05 17:16:09 +00:00
import androidx.test.ext.junit.runners.AndroidJUnit4
2022-07-08 20:28:17 +00:00
import io.mockk.coEvery
import io.mockk.coVerify
2022-03-28 20:13:07 +00:00
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
2022-07-08 22:25:04 +00:00
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
2022-03-28 20:13:07 +00:00
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType
2022-07-08 20:28:17 +00:00
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
import mozilla.components.service.fxa.SyncEngine
2022-03-28 20:13:07 +00:00
import mozilla.components.service.fxa.manager.FxaAccountManager
2022-07-08 20:28:17 +00:00
import mozilla.components.service.fxa.manager.ext.withConstellation
import mozilla.components.service.fxa.store.Account
2022-07-08 22:25:04 +00:00
import mozilla.components.service.fxa.store.SyncAction
import mozilla.components.service.fxa.store.SyncStatus
import mozilla.components.service.fxa.store.SyncStore
2022-07-08 20:28:17 +00:00
import mozilla.components.service.fxa.sync.SyncReason
2022-04-05 17:16:09 +00:00
import mozilla.components.service.glean.testing.GleanTestRule
2022-07-08 22:25:04 +00:00
import mozilla.components.support.test.libstate.ext.waitUntilIdle
2022-04-05 17:16:09 +00:00
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
2022-05-13 13:59:10 +00:00
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
2022-03-28 20:13:07 +00:00
import org.junit.Before
2022-04-05 17:16:09 +00:00
import org.junit.Rule
2022-03-28 20:13:07 +00:00
import org.junit.Test
2022-04-05 17:16:09 +00:00
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.RecentSyncedTabs
2022-03-28 20:13:07 +00:00
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
2022-04-05 17:16:09 +00:00
@RunWith ( AndroidJUnit4 :: class )
2022-03-28 20:13:07 +00:00
class RecentSyncedTabFeatureTest {
2022-04-05 17:16:09 +00:00
@get : Rule
val gleanTestRule = GleanTestRule ( testContext )
2022-03-28 20:13:07 +00:00
private val earliestTime = 100L
private val earlierTime = 250L
private val timeNow = 500L
private val currentDevice = Device (
id = " currentId " ,
displayName = " currentDevice " ,
deviceType = DeviceType . MOBILE ,
isCurrentDevice = true ,
lastAccessTime = timeNow ,
capabilities = listOf ( ) ,
subscriptionExpired = false ,
subscription = null
)
private val deviceAccessed1 = Device (
id = " id1 " ,
displayName = " device1 " ,
deviceType = DeviceType . DESKTOP ,
isCurrentDevice = false ,
lastAccessTime = earliestTime ,
capabilities = listOf ( ) ,
subscriptionExpired = false ,
subscription = null
)
private val deviceAccessed2 = Device (
id = " id2 " ,
displayName = " device2 " ,
deviceType = DeviceType . DESKTOP ,
isCurrentDevice = false ,
lastAccessTime = earlierTime ,
capabilities = listOf ( ) ,
subscriptionExpired = false ,
subscription = null
)
2022-07-08 22:25:04 +00:00
private val appStore : AppStore = mockk ( )
private val accountManager : FxaAccountManager = mockk ( relaxed = true )
2022-07-08 20:28:17 +00:00
private val storage : SyncedTabsStorage = mockk ( )
2022-07-08 22:25:04 +00:00
private val syncStore = SyncStore ( )
2022-03-28 20:13:07 +00:00
private lateinit var feature : RecentSyncedTabFeature
@Before
fun setup ( ) {
2022-07-08 22:25:04 +00:00
Dispatchers . setMain ( StandardTestDispatcher ( ) )
every { appStore . dispatch ( any ( ) ) } returns mockk ( )
2022-03-28 20:13:07 +00:00
feature = RecentSyncedTabFeature (
2022-07-08 22:25:04 +00:00
appStore = appStore ,
syncStore = syncStore ,
2022-03-28 20:13:07 +00:00
accountManager = accountManager ,
2022-07-08 20:28:17 +00:00
storage = storage ,
coroutineScope = TestScope ( ) ,
2022-03-28 20:13:07 +00:00
)
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN account is not available WHEN started THEN nothing is dispatched` ( ) {
feature . start ( )
verify ( exactly = 0 ) { appStore . dispatch ( any ( ) ) }
}
@Test
fun `GIVEN current tab state is none WHEN account becomes available THEN loading state is dispatched, devices are refreshed, and a sync is started` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
2022-07-08 22:25:04 +00:00
every { appStore . state } returns mockk {
2022-04-27 21:06:50 +00:00
every { recentSyncedTabState } returns RecentSyncedTabState . None
}
2022-07-08 20:28:17 +00:00
feature . start ( )
runCurrent ( )
2022-03-28 20:13:07 +00:00
2022-07-08 22:25:04 +00:00
verify { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Loading ) ) }
2022-07-08 20:28:17 +00:00
coVerify { accountManager . withConstellation { refreshDevices ( ) } }
2022-08-10 16:19:31 +00:00
coVerify { accountManager . syncNow ( reason = SyncReason . User , debounce = true , customEngineSubset = listOf ( SyncEngine . Tabs ) ) }
2022-03-28 20:13:07 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN current tab state is not none WHEN account becomes available THEN loading state is not dispatched` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
feature . start ( )
runCurrent ( )
verify ( exactly = 0 ) { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Loading ) ) }
2022-03-28 20:13:07 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN synced tabs WHEN status becomes idle THEN recent synced tab is dispatched` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
val activeTab = createActiveTab ( )
coEvery { storage . getSyncedDeviceTabs ( ) } returns listOf (
SyncedDeviceTabs (
device = deviceAccessed1 ,
tabs = listOf ( activeTab )
)
)
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
val expected = activeTab . toRecentSyncedTab ( deviceAccessed1 )
verify { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Success ( expected ) ) ) }
2022-03-28 20:13:07 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN tabs from remote and current devices WHEN dispatching recent synced tab THEN current device is filtered out of dispatch` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
2022-03-28 20:13:07 +00:00
val localTab = createActiveTab ( " local " , " https://local.com " , null )
val remoteTab = createActiveTab ( " remote " , " https://mozilla.org " , null )
2022-07-08 20:28:17 +00:00
val syncedTabs = listOf (
2022-03-28 20:13:07 +00:00
SyncedDeviceTabs ( currentDevice , listOf ( localTab ) ) ,
SyncedDeviceTabs ( deviceAccessed1 , listOf ( remoteTab ) )
)
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns syncedTabs
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-03-28 20:13:07 +00:00
val expectedTab = remoteTab . toRecentSyncedTab ( deviceAccessed1 )
verify {
2022-07-08 22:25:04 +00:00
appStore . dispatch (
2022-03-28 20:13:07 +00:00
AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Success ( expectedTab ) )
)
}
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN there are devices with empty tabs list WHEN dispatching recent synced tab THEN devices with empty tabs list are filtered out` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
2022-03-28 20:13:07 +00:00
val remoteTab = createActiveTab ( " remote " , " https://mozilla.org " , null )
2022-07-08 20:28:17 +00:00
val syncedTabs = listOf (
2022-03-28 20:13:07 +00:00
SyncedDeviceTabs ( deviceAccessed2 , listOf ( ) ) ,
SyncedDeviceTabs ( deviceAccessed1 , listOf ( remoteTab ) )
)
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns syncedTabs
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-03-28 20:13:07 +00:00
val expectedTab = remoteTab . toRecentSyncedTab ( deviceAccessed1 )
verify {
2022-07-08 22:25:04 +00:00
appStore . dispatch (
2022-03-28 20:13:07 +00:00
AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Success ( expectedTab ) )
)
}
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN tabs from different remote devices WHEN dispatching recent synced tab THEN most recently accessed device is used` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
2022-03-28 20:13:07 +00:00
val firstTab = createActiveTab ( " first " , " https://local.com " , null )
val secondTab = createActiveTab ( " remote " , " https://mozilla.org " , null )
2022-07-08 20:28:17 +00:00
val syncedTabs = listOf (
2022-03-28 20:13:07 +00:00
SyncedDeviceTabs ( deviceAccessed1 , listOf ( firstTab ) ) ,
SyncedDeviceTabs ( deviceAccessed2 , listOf ( secondTab ) )
)
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns syncedTabs
2022-03-28 20:13:07 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-03-28 20:13:07 +00:00
val expectedTab = secondTab . toRecentSyncedTab ( deviceAccessed2 )
verify {
2022-07-08 22:25:04 +00:00
appStore . dispatch (
2022-03-28 20:13:07 +00:00
AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Success ( expectedTab ) )
)
}
}
2022-04-05 17:16:09 +00:00
@Test
2022-07-08 20:28:17 +00:00
fun `WHEN synced tab dispatched THEN labeled counter metric recorded with device type` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . Loading
}
2022-04-05 17:16:09 +00:00
val tab = SyncedDeviceTabs ( deviceAccessed1 , listOf ( createActiveTab ( ) ) )
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns listOf ( tab )
2022-04-05 17:16:09 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-04-05 17:16:09 +00:00
2022-05-30 15:26:37 +00:00
assertEquals ( 1 , RecentSyncedTabs . recentSyncedTabShown [ " desktop " ] . testGetValue ( ) )
2022-04-05 17:16:09 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `WHEN synced tab dispatched THEN load time metric recorded` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
2022-07-08 22:25:04 +00:00
every { appStore . state } returns mockk {
2022-04-27 21:06:50 +00:00
every { recentSyncedTabState } returns RecentSyncedTabState . None
}
2022-04-05 17:16:09 +00:00
val tab = SyncedDeviceTabs ( deviceAccessed1 , listOf ( createActiveTab ( ) ) )
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns listOf ( tab )
2022-04-05 17:16:09 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-04-05 17:16:09 +00:00
2022-05-13 13:59:10 +00:00
assertNotNull ( RecentSyncedTabs . recentSyncedTabTimeToLoad . testGetValue ( ) )
2022-04-05 17:16:09 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN that the dispatched tab was the last dispatched tab WHEN dispatched THEN recorded as stale` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . None
}
2022-04-05 17:16:09 +00:00
val tab = SyncedDeviceTabs ( deviceAccessed1 , listOf ( createActiveTab ( ) ) )
2022-07-08 20:28:17 +00:00
coEvery { storage . getSyncedDeviceTabs ( ) } returns listOf ( tab )
2022-04-05 17:16:09 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
syncStore . setState ( status = SyncStatus . Started )
runCurrent ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-04-05 17:16:09 +00:00
assertEquals ( 1 , RecentSyncedTabs . latestSyncedTabIsStale . testGetValue ( ) )
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN that the dispatched tab was not the last dispatched tab WHEN dispatched THEN not recorded as stale` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . None
}
val tabs1 = listOf ( SyncedDeviceTabs ( deviceAccessed1 , listOf ( createActiveTab ( ) ) ) )
val tabs2 = listOf ( SyncedDeviceTabs ( deviceAccessed2 , listOf ( createActiveTab ( ) ) ) )
coEvery { storage . getSyncedDeviceTabs ( ) } returnsMany listOf ( tabs1 , tabs2 )
2022-04-05 17:16:09 +00:00
2022-07-08 20:28:17 +00:00
feature . start ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
syncStore . setState ( status = SyncStatus . Started )
runCurrent ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
2022-04-05 17:16:09 +00:00
2022-05-13 13:59:10 +00:00
assertNull ( RecentSyncedTabs . latestSyncedTabIsStale . testGetValue ( ) )
2022-04-05 17:16:09 +00:00
}
2022-04-14 17:07:44 +00:00
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN current tab state is loading WHEN error is observed THEN tab state is dispatched as none` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
2022-07-08 22:25:04 +00:00
every { appStore . state } returns mockk {
2022-07-08 20:28:17 +00:00
every { recentSyncedTabState } returnsMany listOf (
RecentSyncedTabState . None ,
RecentSyncedTabState . Loading
)
2022-04-14 17:07:44 +00:00
}
2022-07-08 20:28:17 +00:00
feature . start ( )
runCurrent ( )
syncStore . setState ( status = SyncStatus . Error )
runCurrent ( )
verify { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . None ) ) }
2022-04-14 17:07:44 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN current tab state is not loading WHEN error is observed THEN nothing is dispatched` ( ) = runTest {
feature . start ( )
syncStore . setState ( status = SyncStatus . Error )
runCurrent ( )
2022-04-14 17:07:44 +00:00
2022-07-08 20:28:17 +00:00
verify ( exactly = 0 ) { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . None ) ) }
2022-07-08 22:25:04 +00:00
}
@Test
2022-07-08 20:28:17 +00:00
fun `GIVEN that a tab has been dispatched WHEN LoggedOut is observed THEN tab state is dispatched as none` ( ) = runTest {
val account = mockk < Account > ( )
syncStore . setState ( account = account )
2022-07-08 22:25:04 +00:00
every { appStore . state } returns mockk {
every { recentSyncedTabState } returns RecentSyncedTabState . None
}
2022-07-08 20:28:17 +00:00
val tab = createActiveTab ( )
coEvery { storage . getSyncedDeviceTabs ( ) } returns listOf (
SyncedDeviceTabs ( deviceAccessed1 , listOf ( tab ) )
)
2022-07-08 22:25:04 +00:00
feature . start ( )
2022-07-08 20:28:17 +00:00
runCurrent ( )
syncStore . setState ( status = SyncStatus . Idle )
runCurrent ( )
syncStore . setState ( status = SyncStatus . LoggedOut )
2022-07-08 22:25:04 +00:00
runCurrent ( )
2022-07-08 20:28:17 +00:00
val expected = tab . toRecentSyncedTab ( deviceAccessed1 )
verify { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . Success ( expected ) ) ) }
2022-07-08 22:25:04 +00:00
verify { appStore . dispatch ( AppAction . RecentSyncedTabStateChange ( RecentSyncedTabState . None ) ) }
2022-04-14 17:07:44 +00:00
}
2022-03-28 20:13:07 +00:00
private fun createActiveTab (
title : String = " title " ,
url : String = " url " ,
iconUrl : String ? = null ,
) : Tab {
val tab = mockk < Tab > ( )
val tabEntry = TabEntry ( title , url , iconUrl )
every { tab . active ( ) } returns tabEntry
return tab
}
private fun Tab . toRecentSyncedTab ( device : Device ) = RecentSyncedTab (
deviceDisplayName = device . displayName ,
2022-04-05 17:16:09 +00:00
deviceType = device . deviceType ,
2022-03-28 20:13:07 +00:00
title = this . active ( ) . title ,
url = this . active ( ) . url ,
iconUrl = this . active ( ) . iconUrl
)
2022-07-08 22:25:04 +00:00
private fun SyncStore . setState (
status : SyncStatus ? = null ,
2022-07-08 20:28:17 +00:00
account : Account ? = null ,
2022-07-08 22:25:04 +00:00
) {
status ?. let {
this . dispatch ( SyncAction . UpdateSyncStatus ( status ) )
}
2022-07-08 20:28:17 +00:00
account ?. let {
this . dispatch ( SyncAction . UpdateAccount ( account ) )
}
2022-07-08 22:25:04 +00:00
this . waitUntilIdle ( )
}
2022-03-28 20:13:07 +00:00
}