mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-02 03:40:16 +00:00
[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/22550: Refactor search term tab grouping logic to rely on middleware
This commit is contained in:
parent
d68adfa81b
commit
5cc5b50595
@ -82,6 +82,7 @@ import org.mozilla.fenix.perf.StrictModeManager
|
||||
import org.mozilla.fenix.perf.lazyMonitored
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.settings.advanced.getSelectedLocale
|
||||
import org.mozilla.fenix.tabstray.SearchTermTabGroupMiddleware
|
||||
import org.mozilla.fenix.telemetry.TelemetryMiddleware
|
||||
import org.mozilla.fenix.utils.getUndoDelay
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
@ -210,7 +211,8 @@ class Core(
|
||||
PromptMiddleware(),
|
||||
AdsTelemetryMiddleware(adsTelemetry),
|
||||
LastMediaAccessMiddleware(),
|
||||
HistoryMetadataMiddleware(historyMetadataService)
|
||||
HistoryMetadataMiddleware(historyMetadataService),
|
||||
SearchTermTabGroupMiddleware()
|
||||
)
|
||||
|
||||
BrowserStore(
|
||||
|
@ -4,18 +4,19 @@
|
||||
|
||||
package org.mozilla.fenix.ext
|
||||
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.selectedNormalTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.feature.tabs.ext.hasMediaPlayed
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
import org.mozilla.fenix.tabstray.browser.TabGroup
|
||||
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS_MIN_SIZE
|
||||
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* The time until which a tab is considered in-active (in days).
|
||||
@ -41,7 +42,7 @@ fun BrowserState.asRecentTabs(): List<RecentTab> {
|
||||
} else {
|
||||
listOf(selectedNormalTab)
|
||||
.plus(normalTabs.sortedByDescending { it.lastAccess })
|
||||
.minus(lastTabGroup?.tabs ?: emptyList())
|
||||
.filterNot { lastTabGroup?.tabIds?.contains(it?.id) ?: false }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
@ -80,7 +81,8 @@ val BrowserState.inProgressMediaTab: TabSessionState?
|
||||
* Result will be `null` if the currently open normal tabs are not part of a search group.
|
||||
*/
|
||||
val BrowserState.lastTabGroup: TabGroup?
|
||||
get() = normalTabs.toSearchGroup().first.lastOrNull()
|
||||
get() = tabPartitions[SEARCH_TERM_TAB_GROUPS]?.tabGroups
|
||||
?.lastOrNull { it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE }
|
||||
|
||||
/**
|
||||
* Get the most recent search term group.
|
||||
@ -88,55 +90,18 @@ val BrowserState.lastTabGroup: TabGroup?
|
||||
val BrowserState.lastSearchGroup: RecentTab.SearchGroup?
|
||||
get() {
|
||||
val tabGroup = lastTabGroup ?: return null
|
||||
val firstTab = tabGroup.tabs.firstOrNull() ?: return null
|
||||
val firstTabId = tabGroup.tabIds.firstOrNull() ?: return null
|
||||
val firstTab = findTab(firstTabId) ?: return null
|
||||
|
||||
return RecentTab.SearchGroup(
|
||||
tabGroup.searchTerm,
|
||||
firstTab.id,
|
||||
tabGroup.id,
|
||||
firstTabId,
|
||||
firstTab.content.url,
|
||||
firstTab.content.thumbnail,
|
||||
tabGroup.tabs.count()
|
||||
tabGroup.tabIds.size
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pair containing a list of search term groups sorted by last access time, and "remainder" tabs that have
|
||||
* search terms but should not be in groups (because the group is of size one).
|
||||
*/
|
||||
fun List<TabSessionState>.toSearchGroup(
|
||||
groupSet: Set<String> = emptySet()
|
||||
): Pair<List<TabGroup>, List<TabSessionState>> {
|
||||
val data = filter {
|
||||
it.isNormalTabActiveWithSearchTerm(maxActiveTime)
|
||||
}.groupBy {
|
||||
when {
|
||||
it.content.searchTerms.isNotBlank() -> it.content.searchTerms
|
||||
else -> it.historyMetadata?.searchTerm ?: ""
|
||||
}.lowercase()
|
||||
}
|
||||
|
||||
val groupings = data.map { mapEntry ->
|
||||
val searchTerm = mapEntry.key.replaceFirstChar(Char::uppercase)
|
||||
val groupTabs = mapEntry.value
|
||||
val groupMax = groupTabs.fold(0L) { acc, tab ->
|
||||
max(tab.lastAccess, acc)
|
||||
}
|
||||
|
||||
TabGroup(
|
||||
searchTerm = searchTerm,
|
||||
tabs = groupTabs,
|
||||
lastAccess = groupMax
|
||||
)
|
||||
}
|
||||
|
||||
val groups = groupings
|
||||
.filter { it.tabs.size > 1 || groupSet.contains(it.searchTerm) }
|
||||
.sortedBy { it.lastAccess }
|
||||
val remainderTabs = (groupings - groups).flatMap { it.tabs }
|
||||
|
||||
return groups to remainderTabs
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all inactive tabs based on [maxActiveTime].
|
||||
* The user may have disabled the feature so for user interactions consider using the [actualInactiveTabs] method
|
||||
|
@ -0,0 +1,57 @@
|
||||
/* 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
|
||||
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.HistoryMetadataAction
|
||||
import mozilla.components.browser.state.action.TabGroupAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.getGroupByName
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
|
||||
const val SEARCH_TERM_TAB_GROUPS = "searchTermTabGroups"
|
||||
const val SEARCH_TERM_TAB_GROUPS_MIN_SIZE = 2
|
||||
|
||||
/**
|
||||
* This [Middleware] manages tab groups for search terms.
|
||||
*/
|
||||
class SearchTermTabGroupMiddleware : Middleware<BrowserState, BrowserAction> {
|
||||
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
next: (BrowserAction) -> Unit,
|
||||
action: BrowserAction
|
||||
) {
|
||||
|
||||
next(action)
|
||||
|
||||
when (action) {
|
||||
is HistoryMetadataAction.SetHistoryMetadataKeyAction -> {
|
||||
action.historyMetadataKey.searchTerm?.let { searchTerm ->
|
||||
context.dispatch(
|
||||
TabGroupAction.AddTabAction(SEARCH_TERM_TAB_GROUPS, searchTerm, action.tabId)
|
||||
)
|
||||
}
|
||||
}
|
||||
is HistoryMetadataAction.DisbandSearchGroupAction -> {
|
||||
val group = context.state.tabPartitions[SEARCH_TERM_TAB_GROUPS]?.getGroupByName(action.searchTerm)
|
||||
group?.let {
|
||||
context.dispatch(TabGroupAction.RemoveTabGroupAction(SEARCH_TERM_TAB_GROUPS, it.id))
|
||||
}
|
||||
}
|
||||
is TabListAction.RestoreAction -> {
|
||||
action.tabs.forEach { tab ->
|
||||
tab.historyMetadata?.searchTerm?.let { searchTerm ->
|
||||
context.dispatch(
|
||||
TabGroupAction.AddTabAction(SEARCH_TERM_TAB_GROUPS, searchTerm, tab.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -253,6 +253,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
|
||||
tabsTrayStore
|
||||
),
|
||||
store = requireContext().components.core.store,
|
||||
defaultTabPartitionsFilter = { tabPartitions -> tabPartitions[SEARCH_TERM_TAB_GROUPS] }
|
||||
),
|
||||
owner = this,
|
||||
view = view
|
||||
|
@ -37,14 +37,15 @@ class TabsTrayMiddleware(
|
||||
metrics.track(Event.TabsTrayHasInactiveTabs(action.tabs.size))
|
||||
}
|
||||
}
|
||||
is TabsTrayAction.UpdateSearchGroupTabs -> {
|
||||
is TabsTrayAction.UpdateTabPartitions -> {
|
||||
if (shouldReportSearchGroupMetrics) {
|
||||
shouldReportSearchGroupMetrics = false
|
||||
val tabGroups = action.tabPartition?.tabGroups ?: emptyList()
|
||||
|
||||
metrics.track(Event.SearchTermGroupCount(action.groups.size))
|
||||
metrics.track(Event.SearchTermGroupCount(tabGroups.size))
|
||||
|
||||
if (action.groups.isNotEmpty()) {
|
||||
val tabsPerGroup = action.groups.map { it.tabs.size }
|
||||
if (tabGroups.isNotEmpty()) {
|
||||
val tabsPerGroup = tabGroups.map { it.tabIds.size }
|
||||
val averageTabsPerGroup = tabsPerGroup.average()
|
||||
metrics.track(Event.AverageTabsPerSearchTermGroup(averageTabsPerGroup))
|
||||
|
||||
|
@ -5,12 +5,12 @@
|
||||
package org.mozilla.fenix.tabstray
|
||||
|
||||
import mozilla.components.browser.state.state.ContentState
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.tabstray.browser.TabGroup
|
||||
|
||||
/**
|
||||
* Value type that represents the state of the tabs tray.
|
||||
@ -19,8 +19,8 @@ import org.mozilla.fenix.tabstray.browser.TabGroup
|
||||
* @property mode Whether the browser tab list is in multi-select mode or not with the set of
|
||||
* currently selected tabs.
|
||||
* @property inactiveTabs The list of tabs are considered inactive.
|
||||
* @property searchTermGroups The list of tab groups.
|
||||
* @property normalTabs The list of normal tabs that do not fall under [inactiveTabs] or [searchTermGroups].
|
||||
* @property searchTermPartition The tab partition for search term groups.
|
||||
* @property normalTabs The list of normal tabs that do not fall under [inactiveTabs] or search term groups.
|
||||
* @property privateTabs The list of tabs that are [ContentState.private].
|
||||
* @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired devices.
|
||||
* @property focusGroupTabId The search group tab id to focus. Defaults to null.
|
||||
@ -29,7 +29,7 @@ data class TabsTrayState(
|
||||
val selectedPage: Page = Page.NormalTabs,
|
||||
val mode: Mode = Mode.Normal,
|
||||
val inactiveTabs: List<TabSessionState> = emptyList(),
|
||||
val searchTermGroups: List<TabGroup> = emptyList(),
|
||||
val searchTermPartition: TabPartition? = null,
|
||||
val normalTabs: List<TabSessionState> = emptyList(),
|
||||
val privateTabs: List<TabSessionState> = emptyList(),
|
||||
val syncing: Boolean = false,
|
||||
@ -142,9 +142,9 @@ sealed class TabsTrayAction : Action {
|
||||
data class UpdateInactiveTabs(val tabs: List<TabSessionState>) : TabsTrayAction()
|
||||
|
||||
/**
|
||||
* Updates the list of tab groups in [TabsTrayState.searchTermGroups].
|
||||
* Updates the list of tab groups in [TabsTrayState.searchTermPartition].
|
||||
*/
|
||||
data class UpdateSearchGroupTabs(val groups: List<TabGroup>) : TabsTrayAction()
|
||||
data class UpdateTabPartitions(val tabPartition: TabPartition?) : TabsTrayAction()
|
||||
|
||||
/**
|
||||
* Updates the list of tabs in [TabsTrayState.normalTabs].
|
||||
@ -189,8 +189,8 @@ internal object TabsTrayReducer {
|
||||
state.copy(focusGroupTabId = null)
|
||||
is TabsTrayAction.UpdateInactiveTabs ->
|
||||
state.copy(inactiveTabs = action.tabs)
|
||||
is TabsTrayAction.UpdateSearchGroupTabs ->
|
||||
state.copy(searchTermGroups = action.groups)
|
||||
is TabsTrayAction.UpdateTabPartitions ->
|
||||
state.copy(searchTermPartition = action.tabPartition)
|
||||
is TabsTrayAction.UpdateNormalTabs ->
|
||||
state.copy(normalTabs = action.tabs)
|
||||
is TabsTrayAction.UpdatePrivateTabs ->
|
||||
|
@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
import org.mozilla.fenix.components.Components
|
||||
@ -82,7 +83,7 @@ class InactiveTabsAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
|
||||
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
|
||||
inActiveTabsCount = tabs.size
|
||||
|
||||
// Early return with an empty list to remove the header/footer items.
|
||||
|
@ -51,6 +51,6 @@ class InactiveTabsAutoCloseDialogController(
|
||||
@VisibleForTesting
|
||||
internal fun refreshInactiveTabsSection() {
|
||||
val tabs = browserStore.state.tabs.filter(tabFilter)
|
||||
tray.updateTabs(tabs, browserStore.state.selectedTabId)
|
||||
tray.updateTabs(tabs, null, browserStore.state.selectedTabId)
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class InactiveTabsBinding(
|
||||
.ifChanged()
|
||||
.collect {
|
||||
// We pass null for the selected tab id here, because inactive tabs doesn't care.
|
||||
tray.updateTabs(it, null)
|
||||
tray.updateTabs(it, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,6 @@ class InactiveTabsController(
|
||||
@VisibleForTesting
|
||||
internal fun refreshInactiveTabsSection() {
|
||||
val tabs = tabsTrayStore.state.inactiveTabs
|
||||
tray.updateTabs(tabs, null)
|
||||
tray.updateTabs(tabs, null, null)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
import mozilla.components.lib.state.helpers.AbstractBinding
|
||||
@ -23,11 +22,10 @@ class NormalTabsBinding(
|
||||
private val tabsTray: TabsTray
|
||||
) : AbstractBinding<TabsTrayState>(store) {
|
||||
override suspend fun onState(flow: Flow<TabsTrayState>) {
|
||||
flow.map { it.normalTabs }
|
||||
.ifChanged()
|
||||
flow.ifChanged { Pair(it.normalTabs, it.searchTermPartition) }
|
||||
.collect {
|
||||
// Getting the selectedTabId from the BrowserStore at a different time might lead to a race.
|
||||
tabsTray.updateTabs(it, browserStore.state.selectedTabId)
|
||||
tabsTray.updateTabs(it.normalTabs, it.searchTermPartition, browserStore.state.selectedTabId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import mozilla.components.browser.state.state.isNotEmpty
|
||||
import mozilla.components.lib.state.helpers.AbstractBinding
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
@ -19,9 +20,12 @@ class OtherHeaderBinding(
|
||||
private val showHeader: (Boolean) -> Unit
|
||||
) : AbstractBinding<TabsTrayState>(store) {
|
||||
override suspend fun onState(flow: Flow<TabsTrayState>) {
|
||||
flow.ifAnyChanged { arrayOf(it.normalTabs, it.searchTermGroups) }
|
||||
flow.ifChanged { Pair(it.normalTabs, it.searchTermPartition) }
|
||||
.collect {
|
||||
if (it.searchTermGroups.isNotEmpty() && it.normalTabs.isNotEmpty()) {
|
||||
if (
|
||||
it.normalTabs.isNotEmpty() &&
|
||||
it.searchTermPartition.isNotEmpty()
|
||||
) {
|
||||
showHeader(true)
|
||||
} else {
|
||||
showHeader(false)
|
||||
|
@ -27,7 +27,7 @@ class PrivateTabsBinding(
|
||||
.ifChanged()
|
||||
.collect {
|
||||
// Getting the selectedTabId from the BrowserStore at a different time might lead to a race.
|
||||
tray.updateTabs(it, browserStore.state.selectedTabId)
|
||||
tray.updateTabs(it, null, browserStore.state.selectedTabId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +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 mozilla.components.browser.state.state.TabSessionState
|
||||
|
||||
data class TabGroup(
|
||||
/**
|
||||
* The search term used for the tab group.
|
||||
* Not case dependant - searches with difference letter cases will be part of the same group.
|
||||
* This property's value is then forced to start with an uppercase character.
|
||||
*/
|
||||
val searchTerm: String,
|
||||
|
||||
/**
|
||||
* The list of tabSessionStates belonging to this tab group.
|
||||
*/
|
||||
val tabs: List<TabSessionState>,
|
||||
|
||||
/**
|
||||
* The last time tabs in this group was accessed.
|
||||
*/
|
||||
val lastAccess: Long
|
||||
)
|
@ -12,6 +12,8 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
import androidx.recyclerview.widget.RecyclerView.VERTICAL
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
import org.mozilla.fenix.components.Components
|
||||
@ -76,15 +78,15 @@ class TabGroupAdapter(
|
||||
/**
|
||||
* Not implemented; implementation is handled [List<Tab>.toSearchGroups]
|
||||
*/
|
||||
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) =
|
||||
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) =
|
||||
throw UnsupportedOperationException("Use submitList instead.")
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<TabGroup>() {
|
||||
override fun areItemsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem.searchTerm == newItem.searchTerm
|
||||
override fun areItemsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem.id == newItem.id
|
||||
override fun areContentsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TabGroup.containsTabId(tabId: String): Boolean {
|
||||
return tabs.firstOrNull { it.id == tabId } != null
|
||||
return tabIds.contains(tabId)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray.browser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.lib.state.helpers.AbstractBinding
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
@ -20,10 +21,10 @@ class TabGroupBinding(
|
||||
private val tray: (List<TabGroup>) -> Unit
|
||||
) : AbstractBinding<TabsTrayState>(store) {
|
||||
override suspend fun onState(flow: Flow<TabsTrayState>) {
|
||||
flow.map { it.searchTermGroups }
|
||||
flow.map { it.searchTermPartition?.tabGroups ?: emptyList() }
|
||||
.ifChanged()
|
||||
.collect {
|
||||
tray.invoke(it)
|
||||
tray.invoke(it.filter { tabGroup -> tabGroup.tabIds.isNotEmpty() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ package org.mozilla.fenix.tabstray.browser
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.TabGroupItemBinding
|
||||
@ -36,12 +38,12 @@ class TabGroupViewHolder(
|
||||
private lateinit var groupListAdapter: TabGroupListAdapter
|
||||
|
||||
fun bind(
|
||||
group: TabGroup,
|
||||
tabGroup: TabGroup,
|
||||
) {
|
||||
val selectedTabId = itemView.context.components.core.store.state.selectedTabId
|
||||
val selectedIndex = group.tabs.indexOfFirst { it.id == selectedTabId }
|
||||
val selectedIndex = tabGroup.tabIds.indexOfFirst { it == selectedTabId }
|
||||
|
||||
binding.tabGroupTitle.text = group.searchTerm
|
||||
binding.tabGroupTitle.text = tabGroup.id
|
||||
binding.tabGroupList.apply {
|
||||
layoutManager = LinearLayoutManager(itemView.context, orientation, false)
|
||||
groupListAdapter = TabGroupListAdapter(
|
||||
@ -54,7 +56,11 @@ class TabGroupViewHolder(
|
||||
|
||||
adapter = groupListAdapter
|
||||
|
||||
groupListAdapter.submitList(group.tabs)
|
||||
val tabGroupTabs = itemView.context.components.core.store.state.normalTabs.filter {
|
||||
tabGroup.tabIds.contains(it.id)
|
||||
}
|
||||
|
||||
groupListAdapter.submitList(tabGroupTabs)
|
||||
scrollToPosition(selectedIndex)
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,16 @@
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
import org.mozilla.fenix.ext.maxActiveTime
|
||||
import org.mozilla.fenix.ext.toSearchGroup
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS_MIN_SIZE
|
||||
import org.mozilla.fenix.tabstray.TabsTrayAction
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.ext.hasSearchTerm
|
||||
import org.mozilla.fenix.tabstray.ext.isActive
|
||||
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
@ -25,12 +25,14 @@ class TabSorter(
|
||||
) : TabsTray {
|
||||
private val groupsSet = mutableSetOf<String>()
|
||||
|
||||
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
|
||||
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
|
||||
val privateTabs = tabs.filter { it.content.private }
|
||||
val allNormalTabs = tabs - privateTabs
|
||||
val inactiveTabs = allNormalTabs.getInactiveTabs(settings)
|
||||
val searchTermTabs = allNormalTabs.getSearchGroupTabs(settings)
|
||||
val normalTabs = allNormalTabs - inactiveTabs - searchTermTabs
|
||||
val tabGroups = tabPartition?.getTabGroups(settings, groupsSet) ?: emptyList()
|
||||
val tabGroupTabIds = tabGroups.flatMap { it.tabIds }
|
||||
val normalTabs = (allNormalTabs - inactiveTabs).filterNot { tabGroupTabIds.contains(it.id) }
|
||||
val minTabPartition = tabPartition?.let { TabPartition(tabPartition.id, tabGroups) }
|
||||
|
||||
// Private tabs
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdatePrivateTabs(privateTabs))
|
||||
@ -38,16 +40,14 @@ class TabSorter(
|
||||
// Inactive tabs
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdateInactiveTabs(inactiveTabs))
|
||||
|
||||
// Tab groups
|
||||
val (groups, remainderTabs) = searchTermTabs.toSearchGroup(groupsSet)
|
||||
// Normal tabs
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(normalTabs))
|
||||
|
||||
// Search term tabs
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdateTabPartitions(minTabPartition))
|
||||
|
||||
groupsSet.clear()
|
||||
groupsSet.addAll(groups.map { it.searchTerm })
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdateSearchGroupTabs(groups))
|
||||
|
||||
// Normal tabs.
|
||||
val totalNormalTabs = (normalTabs + remainderTabs)
|
||||
tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(totalNormalTabs))
|
||||
groupsSet.addAll(tabGroups.map { it.id })
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,18 +64,14 @@ private fun List<TabSessionState>.getInactiveTabs(settings: Settings): List<TabS
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of search term tabs based on our preferences.
|
||||
* Returns a list of tab groups based on our preferences.
|
||||
*/
|
||||
private fun List<TabSessionState>.getSearchGroupTabs(settings: Settings): List<TabSessionState> {
|
||||
val inactiveTabsEnabled = settings.inactiveTabsAreEnabled
|
||||
val tabGroupsEnabled = settings.searchTermTabGroupsAreEnabled
|
||||
return when {
|
||||
tabGroupsEnabled && inactiveTabsEnabled ->
|
||||
filter { it.isNormalTabActiveWithSearchTerm(maxActiveTime) }
|
||||
|
||||
tabGroupsEnabled ->
|
||||
filter { it.hasSearchTerm() }
|
||||
|
||||
else -> emptyList()
|
||||
private fun TabPartition.getTabGroups(settings: Settings, groupsSet: Set<String>): List<TabGroup> {
|
||||
return if (settings.searchTermTabGroupsAreEnabled) {
|
||||
tabGroups.filter {
|
||||
it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE || groupsSet.contains(it.id)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray.browser
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
@ -29,7 +30,7 @@ abstract class TabsAdapter<T : TabViewHolder>(
|
||||
protected var styling: TabsTrayStyling = TabsTrayStyling()
|
||||
|
||||
@CallSuper
|
||||
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
|
||||
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
|
||||
this.selectedTabId = selectedTabId
|
||||
|
||||
submitList(tabs)
|
||||
|
@ -7,9 +7,11 @@ package org.mozilla.fenix.tabstray.ext
|
||||
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.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import org.mozilla.fenix.ext.toSearchGroup
|
||||
import org.mozilla.fenix.ext.maxActiveTime
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS_MIN_SIZE
|
||||
|
||||
/**
|
||||
* The currently selected tab if there's one that is private.
|
||||
@ -39,17 +41,21 @@ fun BrowserState.getNormalTrayTabs(
|
||||
searchTermTabGroupsAreEnabled: Boolean,
|
||||
inactiveTabsEnabled: Boolean
|
||||
): List<TabSessionState> {
|
||||
val tabGroupsTabIds = getTabGroups()?.flatMap { it.tabIds } ?: emptyList()
|
||||
return normalTabs.run {
|
||||
if (searchTermTabGroupsAreEnabled && inactiveTabsEnabled) {
|
||||
val remainderTabs = toSearchGroup().second
|
||||
filter { it.isNormalTabActiveWithoutSearchTerm(maxActiveTime) } + remainderTabs
|
||||
filter { it.isNormalTabActive(maxActiveTime) }.filter { tabGroupsTabIds.contains(it.id) }
|
||||
} else if (inactiveTabsEnabled) {
|
||||
filter { it.isNormalTabActive(maxActiveTime) }
|
||||
} else if (searchTermTabGroupsAreEnabled) {
|
||||
val remainderTabs = toSearchGroup().second
|
||||
filter { it.isNormalTabWithSearchTerm() } + remainderTabs
|
||||
filter { it.isNormalTab() }.filter { tabGroupsTabIds.contains(it.id) }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BrowserState.getTabGroups(): List<TabGroup>? {
|
||||
return tabPartitions[SEARCH_TERM_TAB_GROUPS]?.tabGroups
|
||||
?.filter { it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE }
|
||||
}
|
||||
|
@ -26,14 +26,6 @@ internal fun TabSessionState.isNormalTabActive(maxActiveTime: Long): Boolean {
|
||||
return isActive(maxActiveTime) && !content.private
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [TabSessionState] is considered active based on the [maxActiveTime] and
|
||||
* does not have a search term
|
||||
*/
|
||||
internal fun TabSessionState.isNormalTabActiveWithoutSearchTerm(maxActiveTime: Long): Boolean {
|
||||
return isNormalTabActive(maxActiveTime) && !hasSearchTerm()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [TabSessionState] have a search term.
|
||||
*/
|
||||
@ -48,16 +40,16 @@ internal fun TabSessionState.isNormalTabWithSearchTerm(): Boolean {
|
||||
return hasSearchTerm() && !content.private
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [TabSessionState] has a search term but may or may not be active.
|
||||
*/
|
||||
internal fun TabSessionState.isNormalTabWithoutSearchTerm(): Boolean {
|
||||
return !hasSearchTerm() && !content.private
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [TabSessionState] is considered active based on the [maxActiveTime].
|
||||
*/
|
||||
internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean {
|
||||
return !isActive(maxActiveTime) && !content.private
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the [TabSessionState] is not private.
|
||||
*/
|
||||
internal fun TabSessionState.isNormalTab(): Boolean {
|
||||
return !content.private
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.LastMediaAccessState
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.concept.storage.HistoryMetadataKey
|
||||
import org.junit.Assert.assertEquals
|
||||
@ -15,7 +17,7 @@ import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
import org.mozilla.fenix.tabstray.browser.TabGroup
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
class BrowserStateTest {
|
||||
@ -160,7 +162,7 @@ class BrowserStateTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN only normal tabs from a search group are open WHEN recentTabs is called THEN return only the tab group`() {
|
||||
val searchGroupTab = createTab(
|
||||
val searchGroupTab1 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "1",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
@ -169,19 +171,30 @@ class BrowserStateTest {
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
val searchGroupTab2 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "2",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = "https://www.mozilla.org",
|
||||
searchTerm = "Test",
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
val tabGroup = listOf(TabGroup("Test", "", listOf(searchGroupTab1.id, searchGroupTab2.id)))
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(searchGroupTab, searchGroupTab),
|
||||
selectedTabId = searchGroupTab.id
|
||||
tabs = listOf(searchGroupTab1, searchGroupTab2),
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, tabGroup))),
|
||||
selectedTabId = searchGroupTab1.id
|
||||
)
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assert(result[0] is RecentTab.SearchGroup)
|
||||
assertEquals(searchGroupTab.historyMetadata?.searchTerm, (result[0] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals(searchGroupTab.id, (result[0] as RecentTab.SearchGroup).tabId)
|
||||
assertEquals(searchGroupTab.content.url, (result[0] as RecentTab.SearchGroup).url)
|
||||
assertEquals(searchGroupTab.content.thumbnail, (result[0] as RecentTab.SearchGroup).thumbnail)
|
||||
assertEquals(searchGroupTab1.historyMetadata?.searchTerm, (result[0] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals(searchGroupTab1.id, (result[0] as RecentTab.SearchGroup).tabId)
|
||||
assertEquals(searchGroupTab1.content.url, (result[0] as RecentTab.SearchGroup).url)
|
||||
assertEquals(searchGroupTab1.content.thumbnail, (result[0] as RecentTab.SearchGroup).thumbnail)
|
||||
assertEquals(2, (result[0] as RecentTab.SearchGroup).count)
|
||||
}
|
||||
|
||||
@ -199,6 +212,7 @@ class BrowserStateTest {
|
||||
val otherTab = createTab(url = "https://www.mozilla.org/firefox", id = "2")
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(searchGroupTab, otherTab, searchGroupTab),
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("Test", "", listOf("1", "3")))))),
|
||||
selectedTabId = searchGroupTab.id
|
||||
)
|
||||
|
||||
@ -207,7 +221,7 @@ class BrowserStateTest {
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(otherTab, (result[0] as RecentTab.Tab).state)
|
||||
assert(result[1] is RecentTab.SearchGroup)
|
||||
assertEquals(searchGroupTab.historyMetadata?.searchTerm, (result[1] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals("Test", (result[1] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals(searchGroupTab.id, (result[1] as RecentTab.SearchGroup).tabId)
|
||||
assertEquals(searchGroupTab.content.url, (result[1] as RecentTab.SearchGroup).url)
|
||||
assertEquals(searchGroupTab.content.thumbnail, (result[1] as RecentTab.SearchGroup).thumbnail)
|
||||
@ -242,14 +256,14 @@ class BrowserStateTest {
|
||||
|
||||
val result = browserState.asRecentTabs()
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(selectedTab, (result[0] as RecentTab.Tab).state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the selected tab is a normal tab and tab group with two tabs exists WHEN asRecentTabs is called THEN return a list of these tabs`() {
|
||||
val selectedTab = createTab(url = "url", id = "3")
|
||||
val searchGroupTab = createTab(
|
||||
val searchGroupTab1 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "4",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
@ -258,8 +272,19 @@ class BrowserStateTest {
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
val searchGroupTab2 = createTab(
|
||||
url = "https://www.mozilla.org",
|
||||
id = "5",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = "https://www.mozilla.org",
|
||||
searchTerm = "Test",
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
val tabGroup = listOf(TabGroup("Test", "", listOf(searchGroupTab1.id, searchGroupTab2.id)))
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(mockk(relaxed = true), selectedTab, searchGroupTab, searchGroupTab),
|
||||
tabs = listOf(mockk(relaxed = true), selectedTab, searchGroupTab1, searchGroupTab1),
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, tabGroup))),
|
||||
selectedTabId = selectedTab.id
|
||||
)
|
||||
|
||||
@ -268,10 +293,10 @@ class BrowserStateTest {
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(selectedTab, (result[0] as RecentTab.Tab).state)
|
||||
assert(result[1] is RecentTab.SearchGroup)
|
||||
assertEquals(searchGroupTab.historyMetadata?.searchTerm, (result[1] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals(searchGroupTab.id, (result[1] as RecentTab.SearchGroup).tabId)
|
||||
assertEquals(searchGroupTab.content.url, (result[1] as RecentTab.SearchGroup).url)
|
||||
assertEquals(searchGroupTab.content.thumbnail, (result[1] as RecentTab.SearchGroup).thumbnail)
|
||||
assertEquals(searchGroupTab1.historyMetadata?.searchTerm, (result[1] as RecentTab.SearchGroup).searchTerm)
|
||||
assertEquals(searchGroupTab1.id, (result[1] as RecentTab.SearchGroup).tabId)
|
||||
assertEquals(searchGroupTab1.content.url, (result[1] as RecentTab.SearchGroup).url)
|
||||
assertEquals(searchGroupTab1.content.thumbnail, (result[1] as RecentTab.SearchGroup).thumbnail)
|
||||
assertEquals(2, (result[1] as RecentTab.SearchGroup).count)
|
||||
}
|
||||
|
||||
@ -447,14 +472,19 @@ class BrowserStateTest {
|
||||
fun `GIVEN tabs exist with search terms WHEN lastTabGroup is called THEN return the last accessed TabGroup`() {
|
||||
val tab1 = createTab(url = "url1", id = "id1", searchTerms = "test1", lastAccess = 10)
|
||||
val tab2 = createTab(url = "url2", id = "id2", searchTerms = "test1", lastAccess = 11)
|
||||
val tab3 = createTab(url = "url3", id = "id3", searchTerms = "test3", lastAccess = 1000)
|
||||
val tab4 = createTab(url = "url4", id = "id4", searchTerms = "test3", lastAccess = 1111)
|
||||
val tab5 = createTab(url = "url5", id = "id5", searchTerms = "test5", lastAccess = 100)
|
||||
val tab6 = createTab(url = "url6", id = "id6", searchTerms = "test5", lastAccess = 111)
|
||||
val tab3 = createTab(url = "url3", id = "id3", searchTerms = "test3", lastAccess = 100)
|
||||
val tab4 = createTab(url = "url4", id = "id4", searchTerms = "test3", lastAccess = 111)
|
||||
val tab5 = createTab(url = "url5", id = "id5", searchTerms = "test5", lastAccess = 1000)
|
||||
val tab6 = createTab(url = "url6", id = "id6", searchTerms = "test5", lastAccess = 1111)
|
||||
val tabGroup1 = TabGroup("test1", "", listOf(tab1.id, tab2.id))
|
||||
val tabGroup2 = TabGroup("test3", "", listOf(tab3.id, tab4.id))
|
||||
val tabGroup3 = TabGroup("test5", "", listOf(tab5.id, tab6.id))
|
||||
|
||||
val browserState = BrowserState(
|
||||
tabs = listOf(tab1, tab2, tab3, tab4, tab5, tab6)
|
||||
tabs = listOf(tab1, tab2, tab3, tab4, tab5, tab6),
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup1, tabGroup2, tabGroup3))))
|
||||
)
|
||||
val expected = TabGroup("Test3", listOf(tab3, tab4), tab4.lastAccess)
|
||||
val expected = TabGroup("test5", "", listOf(tab5.id, tab6.id))
|
||||
|
||||
val result = browserState.lastTabGroup
|
||||
|
||||
|
@ -14,6 +14,8 @@ import mozilla.components.browser.state.action.MediaSessionAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.LastMediaAccessState
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||
@ -35,6 +37,7 @@ import org.junit.Test
|
||||
import org.mozilla.fenix.home.HomeFragmentAction.RecentTabsChange
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
|
||||
class RecentTabsListFeatureTest {
|
||||
|
||||
@ -407,9 +410,11 @@ class RecentTabsListFeatureTest {
|
||||
)
|
||||
)
|
||||
val tabs = listOf(tab1, tab2)
|
||||
val tabGroup = TabGroup("Test search term", "", listOf(tab1.id, tab2.id))
|
||||
val browserStore = BrowserStore(
|
||||
BrowserState(
|
||||
tabs = tabs,
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup)))),
|
||||
selectedTabId = "1"
|
||||
)
|
||||
)
|
||||
@ -438,7 +443,7 @@ class RecentTabsListFeatureTest {
|
||||
id = "1",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = "https://www.mozilla.org",
|
||||
searchTerm = "test search term",
|
||||
searchTerm = "Test search term",
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
@ -448,15 +453,17 @@ class RecentTabsListFeatureTest {
|
||||
id = "2",
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = "https://www.mozilla.org",
|
||||
searchTerm = "test search term",
|
||||
searchTerm = "Test search term",
|
||||
referrerUrl = "https://www.mozilla.org"
|
||||
)
|
||||
)
|
||||
val tab3 = createTab(url = "https://www.mozilla.org/firefox", id = "3")
|
||||
val tabs = listOf(tab1, tab2, tab3)
|
||||
val tabGroup = TabGroup("Test search term", "", listOf(tab1.id, tab2.id))
|
||||
val browserStore = BrowserStore(
|
||||
BrowserState(
|
||||
tabs = tabs,
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup)))),
|
||||
selectedTabId = "1"
|
||||
)
|
||||
)
|
||||
@ -542,9 +549,11 @@ class RecentTabsListFeatureTest {
|
||||
)
|
||||
)
|
||||
val tabs = listOf(tab1, tab2, tab3)
|
||||
val tabGroup = TabGroup("test search term", "", listOf(tab2.id, tab3.id))
|
||||
val browserStore = BrowserStore(
|
||||
BrowserState(
|
||||
tabs = tabs,
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup)))),
|
||||
selectedTabId = "1"
|
||||
)
|
||||
)
|
||||
@ -561,7 +570,7 @@ class RecentTabsListFeatureTest {
|
||||
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
|
||||
assertEquals(tab1, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
|
||||
val searchGroup = (homeStore.state.recentTabs[1] as RecentTab.SearchGroup)
|
||||
assertEquals(searchGroup.searchTerm, "Test search term")
|
||||
assertEquals(searchGroup.searchTerm, "test search term")
|
||||
assertEquals(searchGroup.tabId, "2")
|
||||
assertEquals(searchGroup.url, "https://www.mozilla.org")
|
||||
assertEquals(searchGroup.thumbnail, null)
|
||||
@ -593,9 +602,11 @@ class RecentTabsListFeatureTest {
|
||||
thumbnail = thumbnail,
|
||||
historyMetadata = historyMetadataKey
|
||||
)
|
||||
val searchTermTabGroup = TabGroup(historyMetadataKey.searchTerm!!, "", listOf(searchTermTab1.id, searchTermTab2.id))
|
||||
val browserStore = BrowserStore(
|
||||
BrowserState(
|
||||
tabs = listOf(mediaTab, selectedTab, searchTermTab1, searchTermTab2),
|
||||
tabPartitions = mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(searchTermTabGroup)))),
|
||||
selectedTabId = "43"
|
||||
)
|
||||
)
|
||||
@ -611,7 +622,7 @@ class RecentTabsListFeatureTest {
|
||||
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
|
||||
assertEquals(selectedTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
|
||||
val searchGroup = (homeStore.state.recentTabs[1] as RecentTab.SearchGroup)
|
||||
assertEquals(searchGroup.searchTerm, "Test search term")
|
||||
assertEquals(searchGroup.searchTerm, "test search term")
|
||||
assertEquals(searchGroup.tabId, "44")
|
||||
assertEquals(searchGroup.url, "https://www.mozilla.org")
|
||||
assertEquals(searchGroup.thumbnail, thumbnail)
|
||||
|
@ -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
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.HistoryMetadataAction
|
||||
import mozilla.components.browser.state.action.TabGroupAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.recover.RecoverableTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.storage.HistoryMetadataKey
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SearchTermTabGroupMiddlewareTest {
|
||||
|
||||
private lateinit var store: BrowserStore
|
||||
private lateinit var searchTermTabGroupMiddleware: SearchTermTabGroupMiddleware
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
searchTermTabGroupMiddleware = SearchTermTabGroupMiddleware()
|
||||
store = BrowserStore(
|
||||
middleware = listOf(searchTermTabGroupMiddleware) + EngineMiddleware.create(engine = mockk()),
|
||||
initialState = BrowserState()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN invoking with set history metadata key action THEN dispatch add tab action`() {
|
||||
val context: MiddlewareContext<BrowserState, BrowserAction> = mockk()
|
||||
val next: (BrowserAction) -> Unit = {}
|
||||
|
||||
every { context.dispatch(any()) } returns Unit
|
||||
|
||||
searchTermTabGroupMiddleware.invoke(
|
||||
context,
|
||||
next,
|
||||
HistoryMetadataAction.SetHistoryMetadataKeyAction("tabId", HistoryMetadataKey("url", "search term", "url"))
|
||||
)
|
||||
|
||||
verify { context.dispatch(TabGroupAction.AddTabAction(SEARCH_TERM_TAB_GROUPS, "search term", "tabId")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN invoking with disband search group action THEN dispatch remove tab group action`() {
|
||||
val context: MiddlewareContext<BrowserState, BrowserAction> = mockk()
|
||||
val next: (BrowserAction) -> Unit = {}
|
||||
val state: BrowserState = mockk()
|
||||
val tabPartitions =
|
||||
mapOf(Pair(SEARCH_TERM_TAB_GROUPS, TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("testId", "search term", listOf("tab1"))))))
|
||||
|
||||
every { context.dispatch(any()) } returns Unit
|
||||
every { context.state } returns state
|
||||
every { state.tabPartitions } returns tabPartitions
|
||||
|
||||
searchTermTabGroupMiddleware.invoke(
|
||||
context,
|
||||
next,
|
||||
HistoryMetadataAction.DisbandSearchGroupAction("search term")
|
||||
)
|
||||
|
||||
verify { context.dispatch(TabGroupAction.RemoveTabGroupAction(SEARCH_TERM_TAB_GROUPS, "testId")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN invoking with restore action THEN dispatch add tab action`() {
|
||||
val context: MiddlewareContext<BrowserState, BrowserAction> = mockk()
|
||||
val next: (BrowserAction) -> Unit = {}
|
||||
|
||||
every { context.dispatch(any()) } returns Unit
|
||||
|
||||
searchTermTabGroupMiddleware.invoke(
|
||||
context,
|
||||
next,
|
||||
TabListAction.RestoreAction(
|
||||
listOf(
|
||||
RecoverableTab(
|
||||
id = "testId",
|
||||
url = "url",
|
||||
historyMetadata = HistoryMetadataKey("url", "search term", "url")
|
||||
)
|
||||
),
|
||||
restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING
|
||||
)
|
||||
)
|
||||
|
||||
verify { context.dispatch(TabGroupAction.AddTabAction(SEARCH_TERM_TAB_GROUPS, "search term", "testId")) }
|
||||
}
|
||||
}
|
@ -7,13 +7,14 @@ package org.mozilla.fenix.tabstray
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.tabstray.browser.TabGroup
|
||||
|
||||
class TabsTrayMiddlewareTest {
|
||||
|
||||
@ -35,21 +36,21 @@ class TabsTrayMiddlewareTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN search term groups are updated AND there is at least one group THEN report the average tabs per group`() {
|
||||
store.dispatch(TabsTrayAction.UpdateSearchGroupTabs(generateSearchTermTabGroupsForAverage()))
|
||||
store.dispatch(TabsTrayAction.UpdateTabPartitions(generateSearchTermTabGroupsForAverage()))
|
||||
store.waitUntilIdle()
|
||||
verify { metrics.track(Event.AverageTabsPerSearchTermGroup(5.0)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN search term groups are updated AND there is at least one group THEN report the distribution of tab sizes`() {
|
||||
store.dispatch(TabsTrayAction.UpdateSearchGroupTabs(generateSearchTermTabGroupsForDistribution()))
|
||||
store.dispatch(TabsTrayAction.UpdateTabPartitions(generateSearchTermTabGroupsForDistribution()))
|
||||
store.waitUntilIdle()
|
||||
verify { metrics.track(Event.SearchTermGroupSizeDistribution(listOf(3L, 2L, 1L, 4L))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN search term groups are updated THEN report the count of search term tab groups`() {
|
||||
store.dispatch(TabsTrayAction.UpdateSearchGroupTabs(emptyList()))
|
||||
store.dispatch(TabsTrayAction.UpdateTabPartitions(null))
|
||||
store.waitUntilIdle()
|
||||
verify { metrics.track(Event.SearchTermGroupCount(0)) }
|
||||
}
|
||||
@ -79,29 +80,29 @@ class TabsTrayMiddlewareTest {
|
||||
assertEquals(4L, tabsTrayMiddleware.generateTabGroupSizeMappedValue(50))
|
||||
}
|
||||
|
||||
private fun generateSearchTermTabGroupsForAverage(): List<TabGroup> {
|
||||
val group1 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
val group2 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
val group3 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
private fun generateSearchTermTabGroupsForAverage(): TabPartition {
|
||||
val group1 = TabGroup("", "", mockk(relaxed = true))
|
||||
val group2 = TabGroup("", "", mockk(relaxed = true))
|
||||
val group3 = TabGroup("", "", mockk(relaxed = true))
|
||||
|
||||
every { group1.tabs.size } returns 8
|
||||
every { group2.tabs.size } returns 4
|
||||
every { group3.tabs.size } returns 3
|
||||
every { group1.tabIds.size } returns 8
|
||||
every { group2.tabIds.size } returns 4
|
||||
every { group3.tabIds.size } returns 3
|
||||
|
||||
return listOf(group1, group2, group3)
|
||||
return TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(group1, group2, group3))
|
||||
}
|
||||
|
||||
private fun generateSearchTermTabGroupsForDistribution(): List<TabGroup> {
|
||||
val group1 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
val group2 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
val group3 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
val group4 = TabGroup("", mockk(relaxed = true), 0L)
|
||||
private fun generateSearchTermTabGroupsForDistribution(): TabPartition {
|
||||
val group1 = TabGroup("", "", mockk(relaxed = true))
|
||||
val group2 = TabGroup("", "", mockk(relaxed = true))
|
||||
val group3 = TabGroup("", "", mockk(relaxed = true))
|
||||
val group4 = TabGroup("", "", mockk(relaxed = true))
|
||||
|
||||
every { group1.tabs.size } returns 8
|
||||
every { group2.tabs.size } returns 4
|
||||
every { group3.tabs.size } returns 2
|
||||
every { group4.tabs.size } returns 12
|
||||
every { group1.tabIds.size } returns 8
|
||||
every { group2.tabIds.size } returns 4
|
||||
every { group3.tabIds.size } returns 2
|
||||
every { group4.tabIds.size } returns 12
|
||||
|
||||
return listOf(group1, group2, group3, group4)
|
||||
return TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(group1, group2, group3, group4))
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ class BrowserTabsAdapterTest {
|
||||
listOf(
|
||||
createTab(url = "url", id = "tab1")
|
||||
),
|
||||
null,
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
@ -79,6 +80,7 @@ class BrowserTabsAdapterTest {
|
||||
|
||||
adapter.updateTabs(
|
||||
listOf(tab),
|
||||
null,
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
|
@ -40,7 +40,7 @@ class InactiveTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.inactiveTabs.isNotEmpty())
|
||||
|
||||
verify { tray.updateTabs(any(), any()) }
|
||||
verify { tray.updateTabs(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -53,6 +53,6 @@ class InactiveTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.inactiveTabs.isEmpty())
|
||||
|
||||
verify { tray.updateTabs(emptyList(), null) }
|
||||
verify { tray.updateTabs(emptyList(), null, null) }
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class InactiveTabsControllerTest {
|
||||
|
||||
appStore.waitUntilIdle()
|
||||
|
||||
verify { tray.updateTabs(capture(tabsSlot), any()) }
|
||||
verify { tray.updateTabs(capture(tabsSlot), null, any()) }
|
||||
|
||||
assertEquals(2, tabsSlot.captured.size)
|
||||
assertEquals("1", tabsSlot.captured.first().id)
|
||||
|
@ -49,7 +49,7 @@ class NormalTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.normalTabs.isNotEmpty())
|
||||
|
||||
verify { tray.updateTabs(capture(slotTabs), "1") }
|
||||
verify { tray.updateTabs(capture(slotTabs), null, "1") }
|
||||
assertEquals(expectedTabs, slotTabs.captured)
|
||||
}
|
||||
|
||||
@ -63,6 +63,6 @@ class NormalTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.normalTabs.isEmpty())
|
||||
|
||||
verify { tray.updateTabs(emptyList(), "1") }
|
||||
verify { tray.updateTabs(emptyList(), null, "1") }
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,15 @@
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
@ -34,7 +37,7 @@ class OtherHeaderBindingTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN tabs for only groups THEN show no header`() {
|
||||
val store = TabsTrayStore(TabsTrayState(searchTermGroups = listOf(mockk())))
|
||||
val store = TabsTrayStore(TabsTrayState(searchTermPartition = mockk()))
|
||||
var result: Boolean? = null
|
||||
val binding = OtherHeaderBinding(store) { result = it }
|
||||
|
||||
@ -60,14 +63,20 @@ class OtherHeaderBindingTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN normal tabs and groups exist THEN show header`() {
|
||||
val store = TabsTrayStore(TabsTrayState(normalTabs = listOf(mockk()), searchTermGroups = listOf(mockk())))
|
||||
var result: Boolean? = null
|
||||
val tabGroup = TabGroup("test", "", listOf("1", "2"))
|
||||
val store = TabsTrayStore(
|
||||
TabsTrayState(
|
||||
normalTabs = listOf(mockk()),
|
||||
searchTermPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup))
|
||||
)
|
||||
)
|
||||
var result = false
|
||||
val binding = OtherHeaderBinding(store) { result = it }
|
||||
|
||||
binding.start()
|
||||
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertTrue(result!!)
|
||||
assertTrue(result)
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ class PrivateTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.privateTabs.isNotEmpty())
|
||||
|
||||
verify { tray.updateTabs(capture(slotTabs), "1") }
|
||||
verify { tray.updateTabs(capture(slotTabs), null, "1") }
|
||||
assertEquals(expectedTabs, slotTabs.captured)
|
||||
}
|
||||
|
||||
@ -64,6 +64,6 @@ class PrivateTabsBindingTest {
|
||||
|
||||
assertTrue(store.state.privateTabs.isEmpty())
|
||||
|
||||
verify { tray.updateTabs(emptyList(), "1") }
|
||||
verify { tray.updateTabs(emptyList(), null, "1") }
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,18 @@
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.tabstray.TabsTrayAction
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
@ -30,28 +34,44 @@ class TabGroupBindingTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN the store is updated THEN notify the adapter`() {
|
||||
val expectedGroups = listOf(TabGroup("cats", emptyList(), 0))
|
||||
val expectedTabGroups = listOf(TabGroup("cats", "name", listOf("1", "2")))
|
||||
val tabPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, expectedTabGroups)
|
||||
|
||||
assertTrue(store.state.searchTermGroups.isEmpty())
|
||||
assertNull(store.state.searchTermPartition?.tabGroups)
|
||||
|
||||
store.dispatch(TabsTrayAction.UpdateSearchGroupTabs(expectedGroups)).joinBlocking()
|
||||
store.dispatch(TabsTrayAction.UpdateTabPartitions(tabPartition)).joinBlocking()
|
||||
|
||||
binding.start()
|
||||
|
||||
assertTrue(store.state.searchTermGroups.isNotEmpty())
|
||||
assertTrue(store.state.searchTermPartition?.tabGroups?.isNotEmpty() == true)
|
||||
|
||||
assertEquals(expectedGroups, captured)
|
||||
assertEquals(expectedTabGroups, captured)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the store is updated with empty tab group THEN notify the adapter`() {
|
||||
val expectedTabPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("cats", "name", emptyList())))
|
||||
|
||||
assertNull(store.state.searchTermPartition?.tabGroups)
|
||||
|
||||
store.dispatch(TabsTrayAction.UpdateTabPartitions(expectedTabPartition)).joinBlocking()
|
||||
|
||||
binding.start()
|
||||
|
||||
assertTrue(store.state.searchTermPartition?.tabGroups?.isNotEmpty() == true)
|
||||
|
||||
assertEquals(emptyList<TabGroup>(), captured)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN non-group tabs are updated THEN do not notify the adapter`() {
|
||||
assertTrue(store.state.searchTermGroups.isEmpty())
|
||||
assertEquals(store.state.searchTermPartition?.tabGroups, null)
|
||||
|
||||
store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
|
||||
|
||||
binding.start()
|
||||
|
||||
assertTrue(store.state.searchTermGroups.isEmpty())
|
||||
assertNull(store.state.searchTermPartition?.tabGroups)
|
||||
|
||||
assertEquals(emptyList<TabGroup>(), captured)
|
||||
}
|
||||
|
@ -6,11 +6,14 @@ package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.state.state.TabGroup
|
||||
import mozilla.components.browser.state.state.TabPartition
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
@ -33,79 +36,59 @@ class TabSorterTest {
|
||||
listOf(
|
||||
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis())
|
||||
),
|
||||
null,
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, null)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN updated with one normal tab and two search term tab THEN adapter have normal tab and a search group`() {
|
||||
val tabSorter = TabSorter(settings, tabsTrayStore)
|
||||
val searchTab1 = createTab(url = "url", id = "tab2", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
val searchTab2 = createTab(url = "url", id = "tab3", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
|
||||
tabSorter.updateTabs(
|
||||
listOf(
|
||||
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
|
||||
createTab(
|
||||
url = "url",
|
||||
id = "tab2",
|
||||
lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
),
|
||||
createTab(
|
||||
url = "url",
|
||||
id = "tab3",
|
||||
lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
searchTab1, searchTab2
|
||||
),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(searchTab1.id, searchTab2.id)))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN updated with one normal tab, one inactive tab and two search term tab THEN adapter have normal tab, inactive tab and a search group`() {
|
||||
val tabSorter = TabSorter(settings, tabsTrayStore)
|
||||
val searchTab1 = createTab(url = "url", id = "tab3", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
val searchTab2 = createTab(url = "url", id = "tab4", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
|
||||
tabSorter.updateTabs(
|
||||
listOf(
|
||||
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
|
||||
createTab(
|
||||
url = "url",
|
||||
id = "tab2",
|
||||
lastAccess = inactiveTimestamp,
|
||||
createdAt = inactiveTimestamp
|
||||
),
|
||||
createTab(
|
||||
url = "url",
|
||||
id = "tab3",
|
||||
lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
),
|
||||
createTab(
|
||||
url = "url",
|
||||
id = "tab4",
|
||||
lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
createTab(url = "url", id = "tab2", lastAccess = inactiveTimestamp, createdAt = inactiveTimestamp),
|
||||
searchTab1, searchTab2
|
||||
),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(searchTab1.id, searchTab2.id)))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
|
||||
}
|
||||
|
||||
@ -136,13 +119,14 @@ class TabSorterTest {
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf("tab3", "tab4")))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 2)
|
||||
}
|
||||
|
||||
@ -173,13 +157,14 @@ class TabSorterTest {
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf("tab3", "tab4")))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 3)
|
||||
}
|
||||
|
||||
@ -210,75 +195,67 @@ class TabSorterTest {
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", mockk()), TabGroup("mozilla", "", mockk()))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN only one search term tab THEN there is no search group`() {
|
||||
val tabSorter = TabSorter(settings, tabsTrayStore)
|
||||
val tab1 =
|
||||
createTab(
|
||||
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
|
||||
tabSorter.updateTabs(
|
||||
listOf(
|
||||
createTab(
|
||||
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
listOf(tab1),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id)))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN remove second last one search term tab THEN search group is kept even if there's only one tab`() {
|
||||
val tabSorter = TabSorter(settings, tabsTrayStore)
|
||||
val tab1 = createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
val tab2 = createTab(url = "url", id = "tab2", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
|
||||
|
||||
tabSorter.updateTabs(
|
||||
listOf(
|
||||
createTab(
|
||||
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
),
|
||||
createTab(
|
||||
url = "url", id = "tab2", lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
listOf(tab1, tab2),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id, tab2.id)))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
|
||||
|
||||
tabSorter.updateTabs(
|
||||
listOf(
|
||||
createTab(
|
||||
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
|
||||
searchTerms = "mozilla"
|
||||
)
|
||||
),
|
||||
listOf(tab1),
|
||||
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id)))),
|
||||
selectedTabId = "tab1"
|
||||
)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
|
||||
assertEquals(tabsTrayStore.state.searchTermGroups.size, 1)
|
||||
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
|
||||
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class AbstractBrowserPageViewHolderTest {
|
||||
viewHolder.bind(adapter)
|
||||
viewHolder.attachedToWindow()
|
||||
|
||||
adapter.updateTabs(listOf(createTab(url = "url", id = "tab1")), "tab1")
|
||||
adapter.updateTabs(listOf(createTab(url = "url", id = "tab1")), null, "tab1")
|
||||
|
||||
assertTrue(trayList.visibility == VISIBLE)
|
||||
assertTrue(emptyList.visibility == GONE)
|
||||
@ -65,7 +65,7 @@ class AbstractBrowserPageViewHolderTest {
|
||||
viewHolder.bind(adapter)
|
||||
viewHolder.attachedToWindow()
|
||||
|
||||
adapter.updateTabs(emptyList(), "")
|
||||
adapter.updateTabs(emptyList(), null, "")
|
||||
|
||||
assertTrue(trayList.visibility == GONE)
|
||||
assertTrue(emptyList.visibility == VISIBLE)
|
||||
|
@ -3,5 +3,5 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
object AndroidComponents {
|
||||
const val VERSION = "98.0.20220111190103"
|
||||
const val VERSION = "98.0.20220112232536"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user