for #24177: sync tabs when home is shown

pull/543/head
MatthewTighe 2 years ago committed by mergify[bot]
parent 38bde17fc4
commit dbfd5ffca0

@ -14,6 +14,7 @@ import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.gleanplumb.Message
@ -63,6 +64,12 @@ sealed class AppAction : Action {
val categoriesSelected: List<PocketRecommendedStoriesSelectedCategory>
) : AppAction()
object RemoveCollectionsPlaceholder : AppAction()
/**
* Updates the [RecentSyncedTabState] with the given [state].
*/
data class RecentSyncedTabStateChange(val state: RecentSyncedTabState) : AppAction()
/**
* [Action]s related to interactions with the Messaging Framework.
*/

@ -15,6 +15,7 @@ import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.gleanplumb.MessagingState
@ -32,6 +33,7 @@ import org.mozilla.fenix.gleanplumb.MessagingState
* @property topSites The list of [TopSite] in the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
* @property recentTabs The list of recent [RecentTab] in the [HomeFragment].
* @property recentSyncedTabState The [RecentSyncedTabState] in the [HomeFragment].
* @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment].
* @property recentHistory The list of [RecentlyVisitedItem]s.
* @property pocketStories The list of currently shown [PocketRecommendedStory]s.
@ -48,6 +50,7 @@ data class AppState(
val topSites: List<TopSite> = emptyList(),
val showCollectionPlaceholder: Boolean = false,
val recentTabs: List<RecentTab> = emptyList(),
val recentSyncedTabState: RecentSyncedTabState = RecentSyncedTabState.None,
val recentBookmarks: List<RecentBookmark> = emptyList(),
val recentHistory: List<RecentlyVisitedItem> = emptyList(),
val pocketStories: List<PocketRecommendedStory> = emptyList(),

@ -75,6 +75,11 @@ internal object AppStoreReducer {
recentTabs = state.recentTabs.filterOutTab(action.recentTab)
)
}
is AppAction.RecentSyncedTabStateChange -> {
state.copy(
recentSyncedTabState = action.state
)
}
is AppAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks)
is AppAction.RemoveRecentBookmark -> {
state.copy(recentBookmarks = state.recentBookmarks.filterNot { it.url == action.recentBookmark.url })

@ -106,6 +106,8 @@ import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabFeature
import org.mozilla.fenix.home.recentsyncedtabs.controller.DefaultRecentSyncedTabController
import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature
import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
import org.mozilla.fenix.home.recentvisits.RecentVisitsFeature
@ -167,6 +169,16 @@ class HomeFragment : Fragment() {
}
}
private val syncedTabFeature by lazy {
RecentSyncedTabFeature(
store = requireComponents.appStore,
context = requireContext(),
storage = requireComponents.backgroundServices.syncedTabsStorage,
accountManager = requireComponents.backgroundServices.accountManager,
lifecycleOwner = viewLifecycleOwner
)
}
private var _sessionControlInteractor: SessionControlInteractor? = null
private val sessionControlInteractor: SessionControlInteractor
get() = _sessionControlInteractor!!
@ -178,6 +190,7 @@ class HomeFragment : Fragment() {
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
private val recentSyncedTabFeature = ViewBoundFeatureWrapper<RecentSyncedTabFeature>()
private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
@ -278,6 +291,14 @@ class HomeFragment : Fragment() {
owner = viewLifecycleOwner,
view = binding.root
)
if (FeatureFlags.taskContinuityFeature) {
recentSyncedTabFeature.set(
feature = syncedTabFeature,
owner = viewLifecycleOwner,
view = binding.root
)
}
}
if (requireContext().settings().showRecentBookmarksFeature) {
@ -340,6 +361,10 @@ class HomeFragment : Fragment() {
store = components.core.store,
appStore = components.appStore,
),
recentSyncedTabController = DefaultRecentSyncedTabController(
addNewTabUseCase = requireComponents.useCases.tabsUseCases.addTab,
navController = findNavController(),
),
recentBookmarksController = DefaultRecentBookmarksController(
activity = activity,
navController = findNavController(),

@ -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 = {},
)
}
}

@ -48,21 +48,6 @@ sealed class RecentTab {
*/
data class Tab(val state: TabSessionState) : RecentTab()
/**
* 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 previewImageUrl The url used to retrieve the preview image of the tab.
*/
data class SyncedTab(
val deviceDisplayName: String,
val title: String,
val url: String,
val previewImageUrl: String?,
) : RecentTab()
/**
* A search term group that was recently viewed
*

@ -5,26 +5,39 @@
package org.mozilla.fenix.home.recenttabs.view
import android.view.View
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor
import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTab
/**
* View holder for a recent tab item.
*
* @param composeView [ComposeView] which will be populated with Jetpack Compose UI content.
* @param interactor [RecentTabInteractor] which will have delegated to all user interactions.
* @param recentTabInteractor [RecentTabInteractor] which will have delegated to all user recent
* tab interactions.
* @param recentSyncedTabInteractor [RecentSyncedTabInteractor] which will have delegated to all user
* recent synced tab interactions.
*/
class RecentTabViewHolder(
composeView: ComposeView,
viewLifecycleOwner: LifecycleOwner,
private val interactor: RecentTabInteractor
private val recentTabInteractor: RecentTabInteractor,
private val recentSyncedTabInteractor: RecentSyncedTabInteractor,
) : ComposeViewHolder(composeView, viewLifecycleOwner) {
init {
@ -40,17 +53,41 @@ class RecentTabViewHolder(
@Composable
override fun Content() {
val recentTabs = components.appStore.observeAsComposableState { state -> state.recentTabs }
val recentSyncedTabState = components.appStore.observeAsComposableState { state -> state.recentSyncedTabState }
RecentTabs(
recentTabs = recentTabs.value ?: emptyList(),
onRecentTabClick = { interactor.onRecentTabClicked(it) },
onRecentSearchGroupClick = { interactor.onRecentSearchGroupClicked(it) },
menuItems = listOf(
RecentTabMenuItem(
title = stringResource(id = R.string.recent_tab_menu_item_remove),
onClick = { tab -> interactor.onRemoveRecentTab(tab) }
Column {
RecentTabs(
recentTabs = recentTabs.value ?: emptyList(),
onRecentTabClick = { recentTabInteractor.onRecentTabClicked(it) },
onRecentSearchGroupClick = { recentTabInteractor.onRecentSearchGroupClicked(it) },
menuItems = listOf(
RecentTabMenuItem(
title = stringResource(id = R.string.recent_tab_menu_item_remove),
onClick = { tab -> recentTabInteractor.onRemoveRecentTab(tab) }
)
)
)
)
recentSyncedTabState.value?.let {
if (FeatureFlags.taskContinuityFeature && it != RecentSyncedTabState.None) {
Spacer(modifier = Modifier.height(8.dp))
val syncedTab = when (it) {
RecentSyncedTabState.None,
RecentSyncedTabState.Loading -> null
is RecentSyncedTabState.Success -> it.tab
}
RecentSyncedTab(
tab = syncedTab,
onRecentSyncedTabClick = { tab ->
recentSyncedTabInteractor.onRecentSyncedTabClicked(tab)
},
onSeeAllSyncedTabsButtonClick = {
recentSyncedTabInteractor.onSyncedTabShowAllClicked()
},
)
}
}
}
}
}

@ -16,7 +16,6 @@ import androidx.compose.foundation.isSystemInDarkTheme
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
@ -27,8 +26,6 @@ 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.DropdownMenu
import androidx.compose.material.DropdownMenuItem
@ -49,7 +46,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
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.unit.dp
import androidx.compose.ui.unit.sp
@ -57,7 +53,6 @@ import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.Image
@ -74,18 +69,13 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param menuItems List of [RecentTabMenuItem] shown long clicking a [RecentTab].
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
* @param onRecentSearchGroupClick Invoked when the user clicks on a recent search group.
* @param onRecentSyncedTabClick Invoked when the user clicks on the recent synced tab.
* @param onSyncedTabSeeAllButtonClick Invoked when user clicks on the "See all" button in the synced tab card.
*/
@Composable
@Suppress("LongParameterList")
fun RecentTabs(
recentTabs: List<RecentTab>,
menuItems: List<RecentTabMenuItem>,
onRecentTabClick: (String) -> Unit = {},
onRecentSearchGroupClick: (String) -> Unit = {},
onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit = {},
onSyncedTabSeeAllButtonClick: () -> Unit = {},
) {
Column(
modifier = Modifier.fillMaxWidth(),
@ -110,15 +100,6 @@ fun RecentTabs(
)
}
}
is RecentTab.SyncedTab -> {
if (FeatureFlags.taskContinuityFeature) {
RecentSyncedTabItem(
tab,
onRecentSyncedTabClick,
onSyncedTabSeeAllButtonClick,
)
}
}
}
}
}
@ -276,124 +257,6 @@ private fun RecentSearchGroupItem(
}
}
/**
* A recent synced tab.
*
* @param tab Optional synced tab. If null, displays placeholders.
* @param onRecentSyncedTabClick Invoked when item is clicked.
* @param onSeeAllButtonClick Invoked when "See all" button is clicked.
*/
@Suppress("LongMethod")
@Composable
private fun RecentSyncedTabItem(
tab: RecentTab.SyncedTab?,
onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit,
onSeeAllButtonClick: () -> 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, 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 = onSeeAllButtonClick,
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 recent tab image.
*
@ -438,19 +301,6 @@ fun RecentTabImage(
}
}
/**
* 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)
)
}
/**
* Menu shown for a [RecentTab.Tab].
*
@ -551,27 +401,3 @@ private fun RecentTabIcon(
}
}
}
/**
* A placeholder for a tab title.
*/
@Composable
private fun RecentTabTitlePlaceholder() {
Column {
TextLinePlaceHolder()
Spacer(modifier = Modifier.height(8.dp))
TextLinePlaceHolder()
}
}
@Composable
private fun TextLinePlaceHolder() {
Box(
modifier = Modifier
.height(12.dp)
.fillMaxWidth()
.background(FirefoxTheme.colors.layer3)
)
}

@ -248,7 +248,8 @@ class SessionControlAdapter(
RecentTabViewHolder.LAYOUT_ID -> return RecentTabViewHolder(
composeView = ComposeView(parent.context),
viewLifecycleOwner = viewLifecycleOwner,
interactor = interactor
recentTabInteractor = interactor,
recentSyncedTabInteractor = interactor,
)
RecentlyVisitedViewHolder.LAYOUT_ID -> return RecentlyVisitedViewHolder(
composeView = ComposeView(parent.context),

@ -17,9 +17,12 @@ import org.mozilla.fenix.home.pocket.PocketStoriesInteractor
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
@ -253,6 +256,7 @@ interface MessageCardInteractor {
class SessionControlInteractor(
private val controller: SessionControlController,
private val recentTabController: RecentTabController,
private val recentSyncedTabController: RecentSyncedTabController,
private val recentBookmarksController: RecentBookmarksController,
private val recentVisitsController: RecentVisitsController,
private val pocketStoriesController: PocketStoriesController
@ -263,6 +267,7 @@ class SessionControlInteractor(
ToolbarInteractor,
MessageCardInteractor,
RecentTabInteractor,
RecentSyncedTabInteractor,
RecentBookmarksInteractor,
RecentVisitsInteractor,
CustomizeHomeIteractor,
@ -384,6 +389,14 @@ class SessionControlInteractor(
recentTabController.handleRecentTabRemoved(tab)
}
override fun onRecentSyncedTabClicked(tab: RecentSyncedTab) {
recentSyncedTabController.handleRecentSyncedTabClick(tab)
}
override fun onSyncedTabShowAllClicked() {
recentSyncedTabController.handleSyncedTabShowAllClicked()
}
override fun onRecentBookmarkClicked(bookmark: RecentBookmark) {
recentBookmarksController.handleBookmarkClicked(bookmark)
}

@ -14,6 +14,7 @@ import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_NORMAL_TABS
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_PRIVATE_TABS
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_SYNCED_TABS
import org.mozilla.fenix.utils.Do
/**
@ -46,9 +47,10 @@ class TabLayoutMediator(
@VisibleForTesting
internal fun selectActivePage() {
val selectedPagerPosition =
when (browsingModeManager.mode.isPrivate) {
true -> POSITION_PRIVATE_TABS
false -> POSITION_NORMAL_TABS
when {
browsingModeManager.mode.isPrivate -> POSITION_PRIVATE_TABS
tabsTrayStore.state.selectedPage == Page.SyncedTabs -> POSITION_SYNCED_TABS
else -> POSITION_NORMAL_TABS
}
selectTabAtPosition(selectedPagerPosition)

@ -36,6 +36,8 @@ import org.mozilla.fenix.home.pocket.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
@ -150,6 +152,25 @@ class AppStoreTest {
assertEquals(listOf(group1, group3, highlight), appStore.state.recentHistory)
}
@Test
fun `GIVEN initial state WHEN recent synced tab state is changed THEN state updated`() = runBlocking {
appStore = AppStore(
AppState(
recentSyncedTabState = RecentSyncedTabState.None
)
)
val loading = RecentSyncedTabState.Loading
appStore.dispatch(AppAction.RecentSyncedTabStateChange(loading)).join()
assertEquals(loading, appStore.state.recentSyncedTabState)
val recentSyncedTab = RecentSyncedTab("device name", "title", "url", null)
val success = RecentSyncedTabState.Success(recentSyncedTab)
appStore.dispatch(AppAction.RecentSyncedTabStateChange(success)).join()
assertEquals(success, appStore.state.recentSyncedTabState)
assertEquals(recentSyncedTab, (appStore.state.recentSyncedTabState as RecentSyncedTabState.Success).tab)
}
@Test
fun `Test changing the history metadata in AppStore`() = runBlocking {
assertEquals(0, appStore.state.recentHistory.size)

@ -18,6 +18,8 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketStoriesController
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
@ -27,6 +29,7 @@ class SessionControlInteractorTest {
private val controller: DefaultSessionControlController = mockk(relaxed = true)
private val recentTabController: RecentTabController = mockk(relaxed = true)
private val recentSyncedTabController: RecentSyncedTabController = mockk(relaxed = true)
private val recentBookmarksController: RecentBookmarksController = mockk(relaxed = true)
private val pocketStoriesController: PocketStoriesController = mockk(relaxed = true)
@ -40,6 +43,7 @@ class SessionControlInteractorTest {
interactor = SessionControlInteractor(
controller,
recentTabController,
recentSyncedTabController,
recentBookmarksController,
recentVisitsController,
pocketStoriesController
@ -171,6 +175,21 @@ class SessionControlInteractorTest {
verify { recentTabController.handleRecentTabShowAllClicked() }
}
@Test
fun `WHEN recent synced tab is clicked THEN the tab is handled`() {
val tab: RecentSyncedTab = mockk()
interactor.onRecentSyncedTabClicked(tab)
verify { recentSyncedTabController.handleRecentSyncedTabClick(tab) }
}
@Test
fun `WHEN recent synced tabs show all is clicked THEN show all synced tabs is handled`() {
interactor.onSyncedTabShowAllClicked()
verify { recentSyncedTabController.handleSyncedTabShowAllClicked() }
}
@Test
fun `WHEN a recently saved bookmark is clicked THEN the selected bookmark is handled`() {
val bookmark = RecentBookmark()

@ -43,7 +43,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -69,7 +69,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -95,7 +95,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -121,7 +121,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -149,7 +149,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = updatedRecentTabs,
recentBookmarks = updatedBookmarks,
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -184,7 +184,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = updatedRecentTabs,
recentBookmarks = updatedBookmarks,
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -236,7 +236,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -263,7 +263,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()
@ -290,7 +290,7 @@ class BlocklistMiddlewareTest {
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
recentHistory = store.state.recentHistory,
)
).joinBlocking()

@ -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)
)
}
}
}

@ -13,6 +13,7 @@ import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.home.pocket.PocketStoriesController
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
@ -24,6 +25,7 @@ class RecentVisitsInteractorTest {
private val defaultSessionControlController: DefaultSessionControlController =
mockk(relaxed = true)
private val recentTabController: RecentTabController = mockk(relaxed = true)
private val recentSyncedTabController: RecentSyncedTabController = mockk(relaxed = true)
private val recentBookmarksController: RecentBookmarksController = mockk(relaxed = true)
private val pocketStoriesController: PocketStoriesController = mockk(relaxed = true)
private val recentVisitsController: RecentVisitsController = mockk(relaxed = true)
@ -35,6 +37,7 @@ class RecentVisitsInteractorTest {
interactor = SessionControlInteractor(
defaultSessionControlController,
recentTabController,
recentSyncedTabController,
recentBookmarksController,
recentVisitsController,
pocketStoriesController

@ -28,8 +28,11 @@ class TabLayoutMediatorTest {
fun `page to normal tab position when mode is also normal`() {
val mediator = TabLayoutMediator(tabLayout, viewPager, interactor, modeManager, tabsTrayStore)
val mockState: TabsTrayState = mockk()
every { modeManager.mode }.answers { BrowsingMode.Normal }
every { tabLayout.getTabAt(POSITION_NORMAL_TABS) }.answers { tab }
every { tabsTrayStore.state } returns mockState
every { mockState.selectedPage } returns Page.NormalTabs
mediator.selectActivePage()
@ -51,6 +54,20 @@ class TabLayoutMediatorTest {
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_PRIVATE_TABS))) }
}
@Test
fun `page to synced tabs when selected page is also synced tabs`() {
val mediator = TabLayoutMediator(tabLayout, viewPager, interactor, modeManager, tabsTrayStore)
val mockState: TabsTrayState = mockk()
every { modeManager.mode }.answers { BrowsingMode.Normal }
every { tabsTrayStore.state } returns mockState
every { mockState.selectedPage } returns Page.SyncedTabs
mediator.selectActivePage()
verify { viewPager.setCurrentItem(POSITION_SYNCED_TABS, false) }
}
@Test
fun `selectTabAtPosition will dispatch the correct TabsTrayStore action`() {
val mediator = TabLayoutMediator(tabLayout, viewPager, interactor, modeManager, tabsTrayStore)

Loading…
Cancel
Save