1
0
Fork 0

For #12287: Add Synced Tabs to Tabs Tray

master
Jonathan Almeida 2020-08-18 00:09:27 -04:00 committed by Jonathan Almeida
parent 2e62dd5c87
commit f614c0b18d
25 changed files with 676 additions and 143 deletions

View File

@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment)
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
}

View File

@ -100,6 +100,7 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
@ -597,6 +598,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
}
/**

View File

@ -10,14 +10,18 @@ import androidx.navigation.NavController
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.NoTabsViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TitleViewHolder
import org.mozilla.fenix.sync.ext.toAdapterList
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice
class SyncedTabsAdapter(
private val listener: (SyncTab) -> Unit
private val newListener: SyncedTabsView.Listener
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
@ -27,30 +31,26 @@ class SyncedTabsAdapter(
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView)
NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
holder.bind(getItem(position), listener)
holder.bind(getItem(position), newListener)
}
override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID
is AdapterItem.Title -> TitleViewHolder.LAYOUT_ID
is AdapterItem.NoTabs -> NoTabsViewHolder.LAYOUT_ID
}
fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
val allDeviceTabs = mutableListOf<AdapterItem>()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
val allDeviceTabs = syncedTabs.toAdapterList()
submitList(allDeviceTabs)
}
@ -59,7 +59,11 @@ class SyncedTabsAdapter(
when (oldItem) {
is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.Tab, is AdapterItem.Error ->
is AdapterItem.NoTabs ->
newItem is AdapterItem.NoTabs && oldItem.device.id == newItem.device.id
is AdapterItem.Tab,
is AdapterItem.Error,
is AdapterItem.Title ->
oldItem == newItem
}
@ -68,9 +72,35 @@ class SyncedTabsAdapter(
oldItem == newItem
}
/**
* The various types of adapter items that can be found in a [SyncedTabsAdapter].
*/
sealed class AdapterItem {
/**
* A title header of the Synced Tabs UI that has a refresh button in it. This may be seen
* only in some views depending on where the Synced Tabs UI is displayed.
*/
object Title : AdapterItem()
/**
* A device header for displaying a synced device.
*/
data class Device(val device: SyncDevice) : AdapterItem()
/**
* A tab that was synced.
*/
data class Tab(val tab: SyncTab) : AdapterItem()
/**
* A placeholder for a device that has no tabs synced.
*/
data class NoTabs(val device: SyncDevice) : AdapterItem()
/**
* A message displayed if an error was encountered.
*/
data class Error(
val descriptionResId: Int,
val navController: NavController? = null

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.sync
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.fragment.app.findFragment
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
@ -18,8 +17,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import java.lang.IllegalStateException
class SyncedTabsLayout @JvmOverloads constructor(
@ -30,7 +32,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
override var listener: SyncedTabsView.Listener? = null
private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) }
private val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val coroutineScope = CoroutineScope(Dispatchers.Main)
init {
@ -53,8 +55,8 @@ class SyncedTabsLayout @JvmOverloads constructor(
null
}
val descriptionResId = stringResourceForError(error)
val errorItem = getErrorItem(navController, error, descriptionResId)
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
adapter.submitList(errorList)
@ -96,27 +98,21 @@ class SyncedTabsLayout @JvmOverloads constructor(
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true
}
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
internal fun getErrorItem(
navController: NavController?,
error: SyncedTabsView.ErrorType,
@StringRes stringResId: Int
): SyncedTabsAdapter.AdapterItem = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE,
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}
}
}
/**
* We have to do this weird daisy-chaining of callbacks because the listener is nullable and
* when we get a null reference, we never get a new binding to the non-null listener.
*/
class ListenerDelegate(
private val listener: (() -> SyncedTabsView.Listener?)
) : SyncedTabsView.Listener {
override fun onRefresh() {
listener.invoke()?.onRefresh()
}
override fun onTabClicked(tab: Tab) {
listener.invoke()?.onTabClicked(tab)
}
}

View File

@ -7,29 +7,36 @@ package org.mozilla.fenix.sync
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.*
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import mozilla.components.browser.storage.sync.Tab
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
* The various view-holders that can be found in a [SyncedTabsAdapter]. For more
* descriptive information on the different types, see the docs for [AdapterItem].
*/
sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit)
abstract fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener)
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindTab(item as AdapterItem.Tab)
itemView.setOnClickListener {
interactor(item.tab)
interactor.onTabClicked(item.tab)
}
}
@ -46,7 +53,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
val errorItem = item as AdapterItem.Error
setErrorMargins()
@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindHeader(item as AdapterItem.Device)
}
@ -93,6 +100,36 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
}
}
class NoTabsViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) = Unit
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_no_item
}
}
class TitleViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
itemView.refresh_icon.setOnClickListener { v ->
val rotation = AnimationUtils.loadAnimation(
itemView.context,
R.anim.full_rotation
).apply {
repeatCount = Animation.ABSOLUTE
}
v.startAnimation(rotation)
interactor.onRefresh()
}
}
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_title
}
}
internal fun setErrorMargins() {
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,

View File

@ -0,0 +1,38 @@
/* 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.sync.ext
import androidx.annotation.StringRes
import androidx.navigation.NavController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter
/**
* Converts the error type to the appropriate matching string resource for displaying to the user.
*/
fun ErrorType.toStringRes() = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
/**
* Converts an error type to an [SyncedTabsAdapter.AdapterItem.Error].
*/
fun ErrorType.toAdapterItem(
@StringRes stringResId: Int,
navController: NavController? = null
) = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}

View File

@ -0,0 +1,22 @@
/* 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.sync.ext
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
fun List<SyncedDeviceTabs>.toAdapterList(
): MutableList<AdapterItem> {
val allDeviceTabs = mutableListOf<AdapterItem>()
forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
return allDeviceTabs
}

View File

@ -0,0 +1,55 @@
/* 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.tabtray
import android.view.View
import androidx.fragment.app.FragmentManager.findFragment
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.sync.ListenerDelegate
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.ext.toAdapterList
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import kotlin.coroutines.CoroutineContext
class SyncedTabsController(
private val view: View,
coroutineContext: CoroutineContext = Dispatchers.Main
) : SyncedTabsView {
override var listener: SyncedTabsView.Listener? = null
val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val scope: CoroutineScope = CoroutineScope(coroutineContext)
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
scope.launch {
val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList()
// Reverse layout for TabTrayView which does things backwards.
adapter.submitList(tabsList.reversed())
}
}
override fun onError(error: SyncedTabsView.ErrorType) {
scope.launch {
val navController: NavController? = try {
findFragment<TabTrayDialogFragment>(view).findNavController()
} catch (exception: IllegalStateException) {
null
}
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
adapter.submitList(listOf(errorItem))
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.engine.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -30,6 +32,7 @@ interface TabTrayController {
fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean)
fun onSyncedTabClicked(syncTab: SyncTab)
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun onCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean
@ -59,6 +62,7 @@ interface TabTrayController {
*/
@Suppress("TooManyFunctions")
class DefaultTabTrayController(
private val activity: HomeActivity,
private val profiler: Profiler?,
private val sessionManager: SessionManager,
private val browsingModeManager: BrowsingModeManager,
@ -117,6 +121,14 @@ class DefaultTabTrayController(
navController.navigate(directions)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
activity.openToBrowserAndLoad(
searchTermOrURL = syncTab.active().url,
newTab = true,
from = BrowserDirection.FromTabTray
)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) {

View File

@ -177,6 +177,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
adapter,
interactor = TabTrayFragmentInteractor(
DefaultTabTrayController(
activity = activity,
profiler = activity.components.core.engine.profiler,
sessionManager = activity.components.core.sessionManager,
browsingModeManager = activity.browsingModeManager,
@ -194,7 +195,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
isPrivate = isPrivate,
startingInLandscape = requireContext().resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE,
lifecycleScope = viewLifecycleOwner.lifecycleScope
lifecycleOwner = viewLifecycleOwner
) { private ->
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.tabtray
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.storage.sync.Tab as SyncTab
@Suppress("TooManyFunctions")
interface TabTrayInteractor {
@ -33,6 +34,11 @@ interface TabTrayInteractor {
*/
fun onCloseAllTabsClicked(private: Boolean)
/**
* Called when the user clicks on a synced tab entry.
*/
fun onSyncedTabClicked(syncTab: SyncTab)
/**
* Called when the physical back button is clicked.
*/
@ -89,6 +95,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.onCloseAllTabsClicked(private)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
controller.onSyncedTabClicked(syncTab)
}
override fun onBackPressed(): Boolean {
return controller.handleBackPressed()
}

View File

@ -16,7 +16,8 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -35,7 +36,10 @@ import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
@ -58,9 +62,10 @@ class TabTrayView(
private val interactor: TabTrayInteractor,
isPrivate: Boolean,
startingInLandscape: Boolean,
lifecycleScope: LifecycleCoroutineScope,
lifecycleOwner: LifecycleOwner,
private val filterTabs: (Boolean) -> Unit
) : LayoutContainer, TabLayout.OnTabSelectedListener {
val lifecycleScope = lifecycleOwner.lifecycleScope
val fabView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_fab, container, true)
@ -79,13 +84,18 @@ class TabTrayView(
private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
private val syncedTabsController = SyncedTabsController(view)
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
private var hasLoaded = false
override val containerView: View?
get() = container
private val components = container.context.components
init {
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
components.analytics.metrics.track(Event.TabsTrayOpened)
toggleFabText(isPrivate)
@ -102,7 +112,7 @@ class TabTrayView(
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
container.context.components.analytics.metrics.track(Event.TabsTrayClosed)
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
@ -135,8 +145,20 @@ class TabTrayView(
setTopOffset(startingInLandscape)
val concatAdapter = ConcatAdapter(tabsAdapter)
syncedTabsFeature.set(
feature = SyncedTabsFeature(
context = container.context,
storage = components.backgroundServices.syncedTabsStorage,
accountManager = components.backgroundServices.accountManager,
view = syncedTabsController,
lifecycleOwner = lifecycleOwner,
onTabClicked = ::handleTabClicked
),
owner = lifecycleOwner,
view = view
)
val concatAdapter = ConcatAdapter(tabsAdapter)
view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply {
reverseLayout = true
@ -156,6 +178,9 @@ class TabTrayView(
// Put the 'Add to collections' button after the tabs have loaded.
concatAdapter.addAdapter(0, collectionsButtonAdapter)
// Put the Synced Tabs adapter at the end.
concatAdapter.addAdapter(0, syncedTabsController.adapter)
if (hasAccessibilityEnabled) {
tabsAdapter.notifyDataSetChanged()
}
@ -193,7 +218,7 @@ class TabTrayView(
}
view.tab_tray_overflow.setOnClickListener {
container.context.components.analytics.metrics.track(Event.TabsTrayMenuOpened)
components.analytics.metrics.track(Event.TabsTrayMenuOpened)
menu = tabTrayItemMenu.menuBuilder.build(container.context)
menu?.show(it)
?.also { pu ->
@ -209,6 +234,10 @@ class TabTrayView(
adjustNewTabButtonsForNormalMode()
}
private fun handleTabClicked(tab: SyncTab) {
interactor.onSyncedTabClicked(tab)
}
private fun adjustNewTabButtonsForNormalMode() {
view.tab_tray_new_tab.apply {
isVisible = hasAccessibilityEnabled
@ -234,7 +263,7 @@ class TabTrayView(
Event.NewTabTapped
}
container.context.components.analytics.metrics.track(eventToSend)
components.analytics.metrics.track(eventToSend)
}
fun expand() {
@ -261,17 +290,14 @@ class TabTrayView(
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (isPrivateModeSelected) {
container.context.components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
} else {
container.context.components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
}
}
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/
}
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
}
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
var mode: Mode = Mode.Normal
private set
@ -513,7 +539,9 @@ class TabTrayView(
// We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`.
val recyclerViewIndex = selectedBrowserTabIndex + collectionsButtonAdapter.itemCount
val recyclerViewIndex = selectedBrowserTabIndex +
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount
layoutManager?.scrollToPosition(recyclerViewIndex)
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="275"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" />

View File

@ -19,6 +19,7 @@
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="viewStart"
android:textColor="@color/tab_tray_item_text_normal_theme"
tools:text="@string/synced_tabs_no_tabs"/>
<com.google.android.material.button.MaterialButton

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/Header16TextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:layout_weight="1"
android:text="@string/synced_tabs" />
<ImageView
android:id="@+id/refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:layout_marginEnd="16dp"
app:srcCompat="@drawable/mozac_ic_refresh"
app:tint="?primaryText" />
</LinearLayout>

View File

@ -0,0 +1,26 @@
/* 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.sync
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.junit.Test
class ListenerDelegateTest {
@Test
fun `delegate invokes nullable listener`() {
val listener: SyncedTabsView.Listener? = mockk(relaxed = true)
val delegate = ListenerDelegate { listener }
delegate.onRefresh()
verify { listener?.onRefresh() }
delegate.onTabClicked(mockk())
verify { listener?.onTabClicked(any()) }
}
}

View File

@ -11,6 +11,7 @@ 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.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@ -21,7 +22,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsAdapterTest {
private lateinit var listener: (Tab) -> Unit
private lateinit var listener: SyncedTabsView.Listener
private lateinit var adapter: SyncedTabsAdapter
private val oneTabDevice = SyncedDeviceTabs(
@ -77,10 +78,12 @@ class SyncedTabsAdapterTest {
fun `updateData() adds items for each device and tab`() {
assertEquals(0, adapter.itemCount)
adapter.updateData(listOf(
oneTabDevice,
threeTabDevice
))
adapter.updateData(
listOf(
oneTabDevice,
threeTabDevice
)
)
assertEquals(5, adapter.itemCount)
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0))

View File

@ -4,16 +4,10 @@
package org.mozilla.fenix.sync
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.R
class SyncedTabsLayoutTest {
@ -25,73 +19,4 @@ class SyncedTabsLayoutTest {
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_NEEDS_REAUTHENTICATION))
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_UNAVAILABLE))
}
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
SyncedTabsLayout.stringResourceForError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_sign_in_message,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_reauth,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
)
assertEquals(
R.string.synced_tabs_no_tabs,
SyncedTabsLayout.stringResourceForError(ErrorType.NO_TABS_AVAILABLE)
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
R.string.synced_tabs_connect_another_device
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
R.string.synced_tabs_enable_tab_syncing
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
R.string.synced_tabs_reauth
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.NO_TABS_AVAILABLE,
R.string.synced_tabs_no_tabs
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_UNAVAILABLE,
R.string.synced_tabs_sign_in_message
)
assertNotNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
}

View File

@ -7,15 +7,18 @@ package org.mozilla.fenix.sync
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
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.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@ -32,6 +35,10 @@ class SyncedTabsViewHolderTest {
private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder
private lateinit var deviceView: View
private lateinit var deviceViewGroupName: TextView
private lateinit var titleView: View
private lateinit var titleViewHolder: SyncedTabsViewHolder.TitleViewHolder
private lateinit var noTabsView: View
private lateinit var noTabsViewHolder: SyncedTabsViewHolder.NoTabsViewHolder
private val tab = Tab(
history = listOf(
@ -59,6 +66,12 @@ class SyncedTabsViewHolderTest {
every { synced_tabs_group_name } returns deviceViewGroupName
}
deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView)
titleView = inflater.inflate(SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID, null)
titleViewHolder = SyncedTabsViewHolder.TitleViewHolder(titleView)
noTabsView = inflater.inflate(SyncedTabsViewHolder.NoTabsViewHolder.LAYOUT_ID, null)
noTabsViewHolder = SyncedTabsViewHolder.NoTabsViewHolder(noTabsView)
}
@Test
@ -71,11 +84,11 @@ class SyncedTabsViewHolderTest {
@Test
fun `TabViewHolder calls interactor on click`() {
val interactor = mockk<(Tab) -> Unit>(relaxed = true)
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor)
tabView.performClick()
verify { interactor(tab) }
verify { interactor.onTabClicked(tab) }
}
@Test
@ -109,4 +122,28 @@ class SyncedTabsViewHolderTest {
)
}
}
@Test
fun `TitleViewHolder calls interactor refresh`() {
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
titleViewHolder.bind(SyncedTabsAdapter.AdapterItem.Title, interactor)
titleView.findViewById<View>(R.id.refresh_icon).performClick()
verify { interactor.onRefresh() }
}
@Test
fun `NoTabsViewHolder does nothing`() {
val device = mockk<Device> {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
}
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
noTabsViewHolder.bind(SyncedTabsAdapter.AdapterItem.NoTabs(device), interactor)
titleView.performClick()
verify { interactor wasNot Called }
}
}

View File

@ -0,0 +1,72 @@
package org.mozilla.fenix.sync.ext
import org.junit.Test
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertEquals
class ErrorTypeKtTest {
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
ErrorType.SYNC_ENGINE_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_sign_in_message,
ErrorType.SYNC_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_reauth,
ErrorType.SYNC_NEEDS_REAUTHENTICATION.toStringRes()
)
assertEquals(
R.string.synced_tabs_no_tabs,
ErrorType.NO_TABS_AVAILABLE.toStringRes()
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_connect_another_device, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_ENGINE_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_enable_tab_syncing, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_NEEDS_REAUTHENTICATION.toAdapterItem(
R.string.synced_tabs_reauth, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = ErrorType.NO_TABS_AVAILABLE.toAdapterItem(
R.string.synced_tabs_no_tabs, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_sign_in_message, navController
)
assertNotNull(errorItem.navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
}

View File

@ -0,0 +1,92 @@
/* 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.sync.ext
import io.mockk.every
import io.mockk.mockk
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.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsAdapter
class SyncedTabsAdapterKtTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = emptyList()
)
private val oneTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = listOf(Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
))
)
private val twoTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Emerald"
every { deviceType } returns DeviceType.MOBILE
},
tabs = listOf(
Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
),
Tab(
history = listOf(
TabEntry(
title = "Firefox",
url = "https://firefox.com",
iconUrl = null
)
),
active = 0,
lastUsed = 0L
)
)
)
@Test
fun `verify ordering of adapter items`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(5, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[2] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[3] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[4] is SyncedTabsAdapter.AdapterItem.Tab)
}
@Test
fun `verify no tabs displayed`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(0, adapterData.count())
}
}

View File

@ -26,6 +26,8 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -34,6 +36,7 @@ import org.mozilla.fenix.ext.sessionsOfType
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultTabTrayControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true)
@ -81,6 +84,7 @@ class DefaultTabTrayControllerTest {
every { tabCollection.title } returns "Collection title"
controller = DefaultTabTrayController(
activity = activity,
profiler = profiler,
sessionManager = sessionManager,
browsingModeManager = browsingModeManager,
@ -156,6 +160,15 @@ class DefaultTabTrayControllerTest {
}
}
@Test
fun onSyncedTabClicked() {
controller.onSyncedTabClicked(mockk(relaxed = true))
verify {
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
}
}
@Test
fun handleBackPressed() {
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(

View File

@ -0,0 +1,78 @@
package org.mozilla.fenix.tabtray
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.sync.SyncedTabsViewHolder
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsControllerTest {
private lateinit var view: View
private lateinit var controller: SyncedTabsController
@Before
fun setup() = runBlockingTest {
view = LayoutInflater.from(testContext).inflate(R.layout.about_list_item, null)
controller = SyncedTabsController(view, coroutineContext)
}
@Test
fun `display synced tabs in reverse`() {
val tabs = listOf(
SyncedDeviceTabs(
device = mockk(relaxed = true),
tabs = listOf(
mockk(relaxed = true),
mockk(relaxed = true)
)
)
)
controller.displaySyncedTabs(tabs)
val itemCount = controller.adapter.itemCount
// title + device name + 2 tabs
assertEquals(4, itemCount)
assertEquals(
SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 1)
)
assertEquals(
SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 2)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 3)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 4)
)
}
@Test
fun `show error when we go kaput`() {
controller.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
assertEquals(1, controller.adapter.itemCount)
assertEquals(
SyncedTabsViewHolder.ErrorViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(0)
)
}
}

View File

@ -53,6 +53,12 @@ class TabTrayFragmentInteractorTest {
verify { controller.onCloseAllTabsClicked(true) }
}
@Test
fun onSyncedTabClicked() {
interactor.onSyncedTabClicked(mockk(relaxed = true))
verify { controller.onSyncedTabClicked(any()) }
}
@Test
fun onBackPressed() {
interactor.onBackPressed()