Issue #20349: Add inactive tab grouping to tabs tray
parent
f8945b3720
commit
69d630f46c
@ -0,0 +1,68 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
/**
|
||||
* The base class for a tabs tray list that wants to display browser tabs.
|
||||
*/
|
||||
abstract class AbstractBrowserTrayList @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : RecyclerView(context, attrs, defStyleAttr) {
|
||||
|
||||
lateinit var interactor: TabsTrayInteractor
|
||||
lateinit var tabsTrayStore: TabsTrayStore
|
||||
|
||||
/**
|
||||
* A [TabsFeature] is required for each browser list to ensure one always exists for displaying
|
||||
* tabs.
|
||||
*/
|
||||
abstract val tabsFeature: TabsFeature
|
||||
|
||||
// NB: The use cases here are duplicated because there isn't a nicer
|
||||
// way to share them without a better dependency injection solution.
|
||||
protected val selectTabUseCase = SelectTabUseCaseWrapper(
|
||||
context.components.analytics.metrics,
|
||||
context.components.useCases.tabsUseCases.selectTab
|
||||
) {
|
||||
interactor.onBrowserTabSelected()
|
||||
}
|
||||
|
||||
protected val removeTabUseCase = RemoveTabUseCaseWrapper(
|
||||
context.components.analytics.metrics
|
||||
) { sessionId ->
|
||||
interactor.onDeleteTab(sessionId)
|
||||
}
|
||||
|
||||
protected val swipeToDelete by lazy {
|
||||
SwipeToDeleteBinding(tabsTrayStore)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
swipeToDelete.start()
|
||||
|
||||
adapter?.onAttachedToRecyclerView(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
swipeToDelete.stop()
|
||||
|
||||
// Notify the adapter that it is released from the view preemptively.
|
||||
adapter?.onDetachedFromRecyclerView(this)
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.InactiveFooterItemBinding
|
||||
import org.mozilla.fenix.databinding.InactiveTabListItemBinding
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.Manual
|
||||
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneDay
|
||||
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneMonth
|
||||
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneWeek
|
||||
|
||||
sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
class HeaderHolder(itemView: View) : InactiveTabViewHolder(itemView) {
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.inactive_header_item
|
||||
}
|
||||
}
|
||||
|
||||
class TabViewHolder(
|
||||
itemView: View,
|
||||
private val browserTrayInteractor: BrowserTrayInteractor
|
||||
) : InactiveTabViewHolder(itemView) {
|
||||
|
||||
private val binding = InactiveTabListItemBinding.bind(itemView)
|
||||
|
||||
fun bind(tab: Tab) {
|
||||
val components = itemView.context.components
|
||||
val makePrettyUrl: (String) -> String = {
|
||||
it.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH)
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
browserTrayInteractor.open(tab)
|
||||
}
|
||||
|
||||
binding.siteListItem.apply {
|
||||
components.core.icons.loadIntoView(iconView, tab.url)
|
||||
setText(tab.title, makePrettyUrl(tab.url))
|
||||
setSecondaryButton(
|
||||
R.drawable.mozac_ic_close,
|
||||
R.string.content_description_close_button
|
||||
) {
|
||||
browserTrayInteractor.close(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.inactive_tab_list_item
|
||||
}
|
||||
}
|
||||
|
||||
class FooterHolder(itemView: View) : InactiveTabViewHolder(itemView) {
|
||||
|
||||
val binding = InactiveFooterItemBinding.bind(itemView)
|
||||
|
||||
fun bind(interval: AutoCloseInterval) {
|
||||
val context = itemView.context
|
||||
val stringRes = when (interval) {
|
||||
Manual, OneDay -> {
|
||||
binding.inactiveDescription.visibility = View.GONE
|
||||
binding.topDivider.visibility = View.GONE
|
||||
null
|
||||
}
|
||||
OneWeek -> {
|
||||
context.getString(interval.description)
|
||||
}
|
||||
OneMonth -> {
|
||||
context.getString(interval.description)
|
||||
}
|
||||
}
|
||||
if (stringRes != null) {
|
||||
binding.inactiveDescription.text =
|
||||
context.getString(R.string.inactive_tabs_description, stringRes)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.inactive_footer_item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AutoCloseInterval(@StringRes val description: Int) {
|
||||
Manual(0),
|
||||
OneDay(0),
|
||||
OneWeek(R.string.inactive_tabs_7_days),
|
||||
OneMonth(R.string.inactive_tabs_30_days)
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import mozilla.components.concept.tabstray.Tab as TabsTrayTab
|
||||
import mozilla.components.concept.tabstray.Tabs
|
||||
import mozilla.components.concept.tabstray.TabsTray
|
||||
import mozilla.components.support.base.observer.ObserverRegistry
|
||||
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.FooterHolder
|
||||
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder
|
||||
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.TabViewHolder
|
||||
import org.mozilla.fenix.tabstray.ext.autoCloseInterval
|
||||
import mozilla.components.support.base.observer.Observable as ComponentObservable
|
||||
|
||||
/**
|
||||
* A convenience alias for readability.
|
||||
*/
|
||||
private typealias Adapter = ListAdapter<InactiveTabsAdapter.Item, InactiveTabViewHolder>
|
||||
|
||||
/**
|
||||
* A convenience alias for readability.
|
||||
*/
|
||||
private typealias Observable = ComponentObservable<TabsTray.Observer>
|
||||
|
||||
/**
|
||||
* The [ListAdapter] for displaying the list of inactive tabs.
|
||||
*/
|
||||
class InactiveTabsAdapter(
|
||||
private val context: Context,
|
||||
private val browserTrayInteractor: BrowserTrayInteractor,
|
||||
delegate: Observable = ObserverRegistry()
|
||||
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(viewType, parent, false)
|
||||
|
||||
return when (viewType) {
|
||||
HeaderHolder.LAYOUT_ID -> HeaderHolder(view)
|
||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor)
|
||||
FooterHolder.LAYOUT_ID -> FooterHolder(view)
|
||||
else -> throw IllegalStateException("Unknown viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: InactiveTabViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is TabViewHolder -> {
|
||||
val item = getItem(position) as Item.Tab
|
||||
holder.bind(item.tab)
|
||||
}
|
||||
is FooterHolder -> {
|
||||
val item = getItem(position) as Item.Footer
|
||||
holder.bind(item.interval)
|
||||
}
|
||||
is HeaderHolder -> {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (position) {
|
||||
0 -> HeaderHolder.LAYOUT_ID
|
||||
itemCount - 1 -> FooterHolder.LAYOUT_ID
|
||||
else -> TabViewHolder.LAYOUT_ID
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTabs(tabs: Tabs) {
|
||||
if (tabs.list.isEmpty()) {
|
||||
// Early return with an empty list to remove the header/footer items.
|
||||
submitList(emptyList())
|
||||
return
|
||||
}
|
||||
|
||||
val items = tabs.list.map { Item.Tab(it) }
|
||||
val footer = Item.Footer(context.autoCloseInterval)
|
||||
|
||||
submitList(listOf(Item.Header) + items + listOf(footer))
|
||||
}
|
||||
|
||||
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
|
||||
override fun onTabsChanged(position: Int, count: Int) = Unit
|
||||
override fun onTabsInserted(position: Int, count: Int) = Unit
|
||||
override fun onTabsMoved(fromPosition: Int, toPosition: Int) = Unit
|
||||
override fun onTabsRemoved(position: Int, count: Int) = Unit
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return if (oldItem is Item.Tab && newItem is Item.Tab) {
|
||||
oldItem.tab.id == newItem.tab.id
|
||||
} else {
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The types of different data we can put into the [InactiveTabsAdapter].
|
||||
*/
|
||||
sealed class Item {
|
||||
|
||||
/**
|
||||
* A title header for the inactive tab section. This may be seen only
|
||||
* when at least one inactive tab is present.
|
||||
*/
|
||||
object Header : Item()
|
||||
|
||||
/**
|
||||
* A tab that is now considered inactive.
|
||||
*/
|
||||
data class Tab(val tab: TabsTrayTab) : Item()
|
||||
|
||||
/**
|
||||
* A footer for the inactive tab section. This may be seen only
|
||||
* when at least one inactive tab is present.
|
||||
*/
|
||||
data class Footer(val interval: AutoCloseInterval) : Item()
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.tabstray.ext.browserAdapter
|
||||
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
|
||||
import org.mozilla.fenix.tabstray.ext.isNormalTabActive
|
||||
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The time until which a tab is considered in-active (in days).
|
||||
*/
|
||||
const val DEFAULT_INACTIVE_DAYS = 4L
|
||||
|
||||
class NormalBrowserTrayList @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : AbstractBrowserTrayList(context, attrs, defStyleAttr) {
|
||||
|
||||
/**
|
||||
* The maximum time from when a tab was created or accessed until it is considered "inactive".
|
||||
*/
|
||||
var maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_INACTIVE_DAYS)
|
||||
|
||||
private val concatAdapter by lazy { adapter as ConcatAdapter }
|
||||
|
||||
override val tabsFeature by lazy {
|
||||
val tabsAdapter = concatAdapter.browserAdapter
|
||||
|
||||
TabsFeature(
|
||||
tabsAdapter,
|
||||
context.components.core.store,
|
||||
selectTabUseCase,
|
||||
removeTabUseCase,
|
||||
{ state ->
|
||||
if (!FeatureFlags.inactiveTabs) {
|
||||
return@TabsFeature !state.content.private
|
||||
}
|
||||
state.isNormalTabActive(maxActiveTime)
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
private val inactiveFeature by lazy {
|
||||
val tabsAdapter = concatAdapter.inactiveTabsAdapter
|
||||
|
||||
TabsFeature(
|
||||
tabsAdapter,
|
||||
context.components.core.store,
|
||||
selectTabUseCase,
|
||||
removeTabUseCase,
|
||||
{ state ->
|
||||
if (!FeatureFlags.inactiveTabs) {
|
||||
return@TabsFeature false
|
||||
}
|
||||
state.isNormalTabInactive(maxActiveTime)
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
private val touchHelper by lazy {
|
||||
TabsTouchHelper(
|
||||
observable = concatAdapter.browserAdapter,
|
||||
onViewHolderTouched = {
|
||||
it is TabViewHolder && swipeToDelete.isSwipeable
|
||||
},
|
||||
onViewHolderDraw = { context.components.settings.gridTabView.not() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
tabsFeature.start()
|
||||
inactiveFeature.start()
|
||||
|
||||
touchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
tabsFeature.stop()
|
||||
inactiveFeature.stop()
|
||||
|
||||
touchHelper.attachToRecyclerView(null)
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/* 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.tabstray.ext
|
||||
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
|
||||
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
|
||||
|
||||
/**
|
||||
* A convenience binding for retrieving the [BrowserTabsAdapter] from the [ConcatAdapter].
|
||||
*/
|
||||
internal val ConcatAdapter.browserAdapter
|
||||
get() = adapters.find { it is BrowserTabsAdapter } as BrowserTabsAdapter
|
||||
|
||||
/**
|
||||
* A convenience binding for retrieving the [InactiveTabsAdapter] from the [ConcatAdapter].
|
||||
*/
|
||||
internal val ConcatAdapter.inactiveTabsAdapter
|
||||
get() = adapters.find { it is InactiveTabsAdapter } as InactiveTabsAdapter
|
@ -0,0 +1,42 @@
|
||||
<?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/. -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/rounded_bottom_corners"
|
||||
android:elevation="@dimen/home_item_elevation"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/top_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?neutralFaded"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/inactive_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_divider"
|
||||
tools:text="@string/inactive_tabs_description" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,35 @@
|
||||
<?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/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/rounded_top_corners"
|
||||
android:clickable="false"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="@dimen/home_item_elevation"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/inactive_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:minLines="1"
|
||||
android:text="@string/inactive_tabs_title"
|
||||
android:textAppearance="@style/Header16TextStyle"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Inactive tabs" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,15 @@
|
||||
<?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/. -->
|
||||
<mozilla.components.ui.widgets.WidgetSiteItemView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/site_list_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?above"
|
||||
android:elevation="@dimen/home_item_elevation"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:minHeight="@dimen/mozac_widget_site_item_height" />
|
@ -1,101 +0,0 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.spyk
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.TrayPagerAdapter
|
||||
import org.mozilla.fenix.tabstray.ext.numberOfGridColumns
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
class DefaultBrowserTrayInteractorTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutdown() {
|
||||
unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN pager position is synced tabs THEN return a list layout manager`() {
|
||||
val interactor =
|
||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk(), mockk())
|
||||
|
||||
val result = interactor.getLayoutManagerForPosition(
|
||||
mockk(),
|
||||
TrayPagerAdapter.POSITION_SYNCED_TABS
|
||||
)
|
||||
|
||||
assertEquals(1, (result as GridLayoutManager).spanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN setting is grid view THEN return grid layout manager`() {
|
||||
val context = mockk<Context>()
|
||||
val settings = mockk<Settings>()
|
||||
val interactor =
|
||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
|
||||
|
||||
every { context.numberOfGridColumns }.answers { 4 }
|
||||
every { settings.gridTabView }.answers { true }
|
||||
|
||||
val result = interactor.getLayoutManagerForPosition(
|
||||
context,
|
||||
TrayPagerAdapter.POSITION_NORMAL_TABS
|
||||
)
|
||||
|
||||
assertEquals(4, (result as GridLayoutManager).spanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN setting is list view THEN return list layout manager`() {
|
||||
val context = mockk<Context>()
|
||||
val settings = mockk<Settings>()
|
||||
val interactor =
|
||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
|
||||
|
||||
every { context.numberOfGridColumns }.answers { 4 }
|
||||
every { settings.gridTabView }.answers { false }
|
||||
|
||||
val result = interactor.getLayoutManagerForPosition(
|
||||
context,
|
||||
TrayPagerAdapter.POSITION_NORMAL_TABS
|
||||
)
|
||||
|
||||
// Should NOT be 4.
|
||||
assertEquals(1, (result as GridLayoutManager).spanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() {
|
||||
val context = mockk<Context>()
|
||||
val resources = mockk<Resources>()
|
||||
val displayMetrics = spyk<DisplayMetrics> {
|
||||
widthPixels = 1
|
||||
density = 1f
|
||||
}
|
||||
every { context.resources } returns resources
|
||||
every { resources.displayMetrics } returns displayMetrics
|
||||
|
||||
val result = context.numberOfGridColumns
|
||||
|
||||
assertEquals(2, result)
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/* 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.tabstray.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.spyk
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ContextKtTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() {
|
||||
mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||
|
||||
val context = mockk<Context>()
|
||||
val resources = mockk<Resources>()
|
||||
val displayMetrics = spyk<DisplayMetrics> {
|
||||
widthPixels = 1
|
||||
density = 1f
|
||||
}
|
||||
every { context.resources } returns resources
|
||||
every { resources.displayMetrics } returns displayMetrics
|
||||
|
||||
val result = context.numberOfGridColumns
|
||||
|
||||
assertEquals(2, result)
|
||||
|
||||
unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/* 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.tabstray.ext
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.ContentState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.NORMAL
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.PRIVATE
|
||||
|
||||
class TabSessionStateKtTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN configuration is private THEN return true`() {
|
||||
val contentState = mockk<ContentState>()
|
||||
val state = TabSessionState(content = contentState)
|
||||
val config = PRIVATE
|
||||
|
||||
every { contentState.private } returns true
|
||||
|
||||
assertTrue(state.filterFromConfig(config))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN configuration is normal THEN return false`() {
|
||||
val contentState = mockk<ContentState>()
|
||||
val state = TabSessionState(content = contentState)
|
||||
val config = NORMAL
|
||||
|
||||
every { contentState.private } returns false
|
||||
|
||||
assertTrue(state.filterFromConfig(config))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN configuration does not match THEN return false`() {
|
||||
val contentState = mockk<ContentState>()
|
||||
val state = TabSessionState(content = contentState)
|
||||
val config = NORMAL
|
||||
|
||||
every { contentState.private } returns true
|
||||
|
||||
assertFalse(state.filterFromConfig(config))
|
||||
|
||||
val config2 = PRIVATE
|
||||
|
||||
every { contentState.private } returns false
|
||||
|
||||
assertFalse(state.filterFromConfig(config2))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue