[fenix] for https://github.com/mozilla-mobile/fenix/issues/24177: sync tabs when home is shown
parent
ada37de7d7
commit
690b04b572
@ -0,0 +1,122 @@
|
||||
/* 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
|
||||
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
|
||||
/**
|
||||
* Delegate to handle layout updates and dispatch actions related to the recent synced tab.
|
||||
*
|
||||
* @property store Store to dispatch actions to when synced tabs are updated or errors encountered.
|
||||
* @param accountManager Account manager used to retrieve synced tab state.
|
||||
* @param context [Context] used for retrieving the sync engine storage state.
|
||||
* @param storage Storage layer for synced tabs.
|
||||
* @param lifecycleOwner View lifecycle owner to determine start/stop state for feature.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class RecentSyncedTabFeature(
|
||||
private val store: AppStore,
|
||||
accountManager: FxaAccountManager,
|
||||
context: Context,
|
||||
storage: SyncedTabsStorage,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : SyncedTabsView, LifecycleAwareFeature {
|
||||
private val syncedTabsFeature by lazy {
|
||||
SyncedTabsFeature(
|
||||
view = this,
|
||||
context = context,
|
||||
storage = storage,
|
||||
accountManager = accountManager,
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
onTabClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
override var listener: SyncedTabsView.Listener? = null
|
||||
|
||||
override fun startLoading() {
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Loading)
|
||||
)
|
||||
}
|
||||
|
||||
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
|
||||
val syncedTab = syncedTabs
|
||||
.filterNot { it.device.isCurrentDevice || it.tabs.isEmpty() }
|
||||
.maxByOrNull { it.device.lastAccessTime ?: 0 }
|
||||
?.let {
|
||||
val tab = it.tabs.firstOrNull()?.active() ?: return
|
||||
RecentSyncedTab(
|
||||
deviceDisplayName = it.device.displayName,
|
||||
title = tab.title,
|
||||
url = tab.url,
|
||||
iconUrl = tab.iconUrl
|
||||
)
|
||||
} ?: return
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Success(syncedTab))
|
||||
)
|
||||
}
|
||||
|
||||
// UI will either not be displayed if not authenticated (DefaultPresenter.start),
|
||||
// or the display state will be tied directly to the success and error cases.
|
||||
override fun stopLoading() = Unit
|
||||
|
||||
override fun onError(error: SyncedTabsView.ErrorType) {
|
||||
store.dispatch(AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.None))
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
syncedTabsFeature.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
syncedTabsFeature.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the recent synced tab.
|
||||
*/
|
||||
sealed class RecentSyncedTabState {
|
||||
/**
|
||||
* There is no synced tab, or a user is not authenticated.
|
||||
*/
|
||||
object None : RecentSyncedTabState()
|
||||
|
||||
/**
|
||||
* A user is authenticated and the sync is running.
|
||||
*/
|
||||
object Loading : RecentSyncedTabState()
|
||||
|
||||
/**
|
||||
* A user is authenticated and the most recent synced tab has been found.
|
||||
*/
|
||||
data class Success(val tab: RecentSyncedTab) : RecentSyncedTabState()
|
||||
}
|
||||
|
||||
/**
|
||||
* A tab that was recently viewed on a synced device.
|
||||
*
|
||||
* @param deviceDisplayName The device the tab was viewed on.
|
||||
* @param title The title of the tab.
|
||||
* @param url The url of the tab.
|
||||
* @param iconUrl The url used to retrieve the icon of the tab.
|
||||
*/
|
||||
data class RecentSyncedTab(
|
||||
val deviceDisplayName: String,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val iconUrl: String?,
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
/* 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.controller
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
|
||||
import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor
|
||||
import org.mozilla.fenix.tabstray.Page
|
||||
|
||||
/**
|
||||
* An interface that handles the view manipulation of the recent synced tabs in the Home screen.
|
||||
*/
|
||||
interface RecentSyncedTabController {
|
||||
/**
|
||||
* @see [RecentSyncedTabInteractor.onRecentSyncedTabClicked]
|
||||
*/
|
||||
fun handleRecentSyncedTabClick(tab: RecentSyncedTab)
|
||||
|
||||
/**
|
||||
* @see [RecentSyncedTabInteractor.onRecentSyncedTabClicked]
|
||||
*/
|
||||
fun handleSyncedTabShowAllClicked()
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of [RecentSyncedTabController].
|
||||
*
|
||||
* @property addNewTabUseCase Use case to open the synced tab when clicked.
|
||||
* @property navController [NavController] to navigate to synced tabs tray.
|
||||
*/
|
||||
class DefaultRecentSyncedTabController(
|
||||
private val addNewTabUseCase: TabsUseCases.AddNewTabUseCase,
|
||||
private val navController: NavController,
|
||||
) : RecentSyncedTabController {
|
||||
override fun handleRecentSyncedTabClick(tab: RecentSyncedTab) {
|
||||
addNewTabUseCase.invoke(tab.url)
|
||||
navController.navigate(R.id.browserFragment)
|
||||
}
|
||||
|
||||
override fun handleSyncedTabShowAllClicked() {
|
||||
navController.navigate(
|
||||
HomeFragmentDirections.actionGlobalTabsTrayFragment(page = Page.SyncedTabs)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/* 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.interactor
|
||||
|
||||
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
|
||||
|
||||
/**
|
||||
* Interface for recent synced tab related actions in the Home screen.
|
||||
*/
|
||||
interface RecentSyncedTabInteractor {
|
||||
/**
|
||||
* Opens the synced tab locally. Called when a user clicks on a recent synced tab.
|
||||
*
|
||||
* @param tab The recent synced tab that has been clicked.
|
||||
*/
|
||||
fun onRecentSyncedTabClicked(tab: RecentSyncedTab)
|
||||
|
||||
/**
|
||||
* Opens the tabs tray to the synced tab page. Called when a user clicks on the "See all synced
|
||||
* tabs" button.
|
||||
*/
|
||||
fun onSyncedTabShowAllClicked()
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
/* 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.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.PrimaryText
|
||||
import org.mozilla.fenix.compose.SecondaryText
|
||||
import org.mozilla.fenix.compose.ThumbnailCard
|
||||
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
|
||||
/**
|
||||
* A recent synced tab card.
|
||||
*
|
||||
* @param tab The [RecentSyncedTab] to display.
|
||||
* @param onRecentSyncedTabClick Invoked when the user clicks on the recent synced tab.
|
||||
* @param onSeeAllSyncedTabsButtonClick Invoked when user clicks on the "See all" button in the synced tab card.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun RecentSyncedTab(
|
||||
tab: RecentSyncedTab?,
|
||||
onRecentSyncedTabClick: (RecentSyncedTab) -> Unit,
|
||||
onSeeAllSyncedTabsButtonClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(180.dp)
|
||||
.clickable { tab?.let { onRecentSyncedTabClick(tab) } },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
backgroundColor = FirefoxTheme.colors.layer2,
|
||||
elevation = 6.dp
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
if (tab == null) {
|
||||
RecentTabImagePlaceholder()
|
||||
} else {
|
||||
ThumbnailCard(
|
||||
url = tab.url,
|
||||
key = tab.url.hashCode().toString(),
|
||||
modifier = Modifier
|
||||
.size(108.dp, 80.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
) {
|
||||
if (tab == null) {
|
||||
RecentTabTitlePlaceholder()
|
||||
} else {
|
||||
PrimaryText(
|
||||
text = tab.title,
|
||||
fontSize = 14.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (tab == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(FirefoxTheme.colors.layer3)
|
||||
.size(18.dp)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_synced_tabs),
|
||||
contentDescription = stringResource(
|
||||
R.string.recent_tabs_synced_device_icon_content_description
|
||||
),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
if (tab == null) {
|
||||
TextLinePlaceHolder()
|
||||
} else {
|
||||
SecondaryText(
|
||||
text = tab.deviceDisplayName,
|
||||
fontSize = 12.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onSeeAllSyncedTabsButtonClick,
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
backgroundColor = if (tab == null) {
|
||||
FirefoxTheme.colors.layer3
|
||||
} else {
|
||||
FirefoxTheme.colors.actionSecondary
|
||||
}
|
||||
),
|
||||
elevation = ButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
if (tab != null) {
|
||||
Text(
|
||||
text = stringResource(R.string.recent_tabs_see_all_synced_tabs_button_text),
|
||||
textAlign = TextAlign.Center,
|
||||
color = FirefoxTheme.colors.textActionSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder for a recent tab image.
|
||||
*/
|
||||
@Composable
|
||||
private fun RecentTabImagePlaceholder() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(108.dp, 80.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color = FirefoxTheme.colors.layer3)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder for a tab title.
|
||||
*/
|
||||
@Composable
|
||||
private fun RecentTabTitlePlaceholder() {
|
||||
Column {
|
||||
TextLinePlaceHolder()
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextLinePlaceHolder()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder for a single line of text.
|
||||
*/
|
||||
@Composable
|
||||
private fun TextLinePlaceHolder() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(12.dp)
|
||||
.fillMaxWidth()
|
||||
.background(FirefoxTheme.colors.layer3)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LoadedRecentSyncedTab() {
|
||||
val tab = RecentSyncedTab(
|
||||
deviceDisplayName = "Firefox on MacBook",
|
||||
title = "This is a long site title",
|
||||
url = "https://mozilla.org",
|
||||
iconUrl = "https://mozilla.org",
|
||||
)
|
||||
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
|
||||
RecentSyncedTab(
|
||||
tab = tab,
|
||||
onRecentSyncedTabClick = {},
|
||||
onSeeAllSyncedTabsButtonClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LoadingRecentSyncedTab() {
|
||||
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
|
||||
RecentSyncedTab(
|
||||
tab = null,
|
||||
onRecentSyncedTabClick = {},
|
||||
onSeeAllSyncedTabsButtonClick = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
/* 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
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
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
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
|
||||
class RecentSyncedTabFeatureTest {
|
||||
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
|
||||
)
|
||||
|
||||
private val store: AppStore = mockk()
|
||||
private val accountManager: FxaAccountManager = mockk()
|
||||
|
||||
private lateinit var feature: RecentSyncedTabFeature
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
every { store.dispatch(any()) } returns mockk()
|
||||
|
||||
feature = RecentSyncedTabFeature(
|
||||
store = store,
|
||||
accountManager = accountManager,
|
||||
context = mockk(),
|
||||
storage = mockk(),
|
||||
lifecycleOwner = mockk(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN loading is started THEN loading state is dispatched`() {
|
||||
feature.startLoading()
|
||||
|
||||
verify { store.dispatch(AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Loading)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN empty synced tabs are displayed THEN no action is dispatched`() {
|
||||
feature.displaySyncedTabs(listOf())
|
||||
|
||||
verify(exactly = 0) { store.dispatch(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN displaying synced tabs THEN first active tab is used`() {
|
||||
val tab = createActiveTab("title", "https://mozilla.org", null)
|
||||
val displayedTabs = listOf(SyncedDeviceTabs(deviceAccessed1, listOf(tab)))
|
||||
|
||||
feature.displaySyncedTabs(displayedTabs)
|
||||
|
||||
val expectedTab = tab.toRecentSyncedTab(deviceAccessed1)
|
||||
|
||||
verify {
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Success(expectedTab))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN displaying synced tabs THEN current device is filtered out`() {
|
||||
val localTab = createActiveTab("local", "https://local.com", null)
|
||||
val remoteTab = createActiveTab("remote", "https://mozilla.org", null)
|
||||
val displayedTabs = listOf(
|
||||
SyncedDeviceTabs(currentDevice, listOf(localTab)),
|
||||
SyncedDeviceTabs(deviceAccessed1, listOf(remoteTab))
|
||||
)
|
||||
|
||||
feature.displaySyncedTabs(displayedTabs)
|
||||
|
||||
val expectedTab = remoteTab.toRecentSyncedTab(deviceAccessed1)
|
||||
|
||||
verify {
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Success(expectedTab))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN displaying synced tabs THEN any devices with empty tabs list are filtered out`() {
|
||||
val remoteTab = createActiveTab("remote", "https://mozilla.org", null)
|
||||
val displayedTabs = listOf(
|
||||
SyncedDeviceTabs(deviceAccessed2, listOf()),
|
||||
SyncedDeviceTabs(deviceAccessed1, listOf(remoteTab))
|
||||
)
|
||||
|
||||
feature.displaySyncedTabs(displayedTabs)
|
||||
|
||||
val expectedTab = remoteTab.toRecentSyncedTab(deviceAccessed1)
|
||||
|
||||
verify {
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Success(expectedTab))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN displaying synced tabs THEN most recently accessed device is used`() {
|
||||
val firstTab = createActiveTab("first", "https://local.com", null)
|
||||
val secondTab = createActiveTab("remote", "https://mozilla.org", null)
|
||||
val displayedTabs = listOf(
|
||||
SyncedDeviceTabs(deviceAccessed1, listOf(firstTab)),
|
||||
SyncedDeviceTabs(deviceAccessed2, listOf(secondTab))
|
||||
)
|
||||
|
||||
feature.displaySyncedTabs(displayedTabs)
|
||||
|
||||
val expectedTab = secondTab.toRecentSyncedTab(deviceAccessed2)
|
||||
|
||||
verify {
|
||||
store.dispatch(
|
||||
AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.Success(expectedTab))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN error is received THEN action dispatched with empty synced state`() {
|
||||
feature.onError(SyncedTabsView.ErrorType.NO_TABS_AVAILABLE)
|
||||
|
||||
verify { store.dispatch(AppAction.RecentSyncedTabStateChange(RecentSyncedTabState.None)) }
|
||||
}
|
||||
|
||||
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,
|
||||
title = this.active().title,
|
||||
url = this.active().url,
|
||||
iconUrl = this.active().iconUrl
|
||||
)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/* 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.controller
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
|
||||
import org.mozilla.fenix.tabstray.Page
|
||||
|
||||
class DefaultRecentSyncedTabControllerTest {
|
||||
|
||||
private val addTabUseCase: TabsUseCases.AddNewTabUseCase = mockk()
|
||||
private val navController: NavController = mockk()
|
||||
|
||||
private lateinit var controller: RecentSyncedTabController
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
controller = DefaultRecentSyncedTabController(addTabUseCase, navController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN synced tab clicked THEN tab add and navigate to browser`() {
|
||||
val url = "https://mozilla.org"
|
||||
val tab = RecentSyncedTab(
|
||||
deviceDisplayName = "display",
|
||||
title = "title",
|
||||
url = url,
|
||||
iconUrl = null
|
||||
)
|
||||
|
||||
every { addTabUseCase.invoke(any()) } just runs
|
||||
every { navController.navigate(any<Int>()) } just runs
|
||||
|
||||
controller.handleRecentSyncedTabClick(tab)
|
||||
|
||||
verify { addTabUseCase.invoke(url) }
|
||||
verify { navController.navigate(R.id.browserFragment) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN synced tab show all clicked THEN navigate to synced tabs tray`() {
|
||||
every { navController.navigate(any<NavDirections>()) } just runs
|
||||
|
||||
controller.handleSyncedTabShowAllClicked()
|
||||
|
||||
verify {
|
||||
navController.navigate(
|
||||
HomeFragmentDirections.actionGlobalTabsTrayFragment(page = Page.SyncedTabs)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue