diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 136da1ee0..5fc8ac26f 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt b/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt index 88c578d99..deaddfc9d 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt @@ -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 { } 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.toSearchGroup( - groupSet: Set = emptySet() -): Pair, List> { - 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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddleware.kt b/app/src/main/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddleware.kt new file mode 100644 index 000000000..6a7bd8c4a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddleware.kt @@ -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 { + + override fun invoke( + context: MiddlewareContext, + 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) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 4987d8fbc..359476668 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -253,6 +253,7 @@ class TabsTrayFragment : AppCompatDialogFragment() { tabsTrayStore ), store = requireContext().components.core.store, + defaultTabPartitionsFilter = { tabPartitions -> tabPartitions[SEARCH_TERM_TAB_GROUPS] } ), owner = this, view = view diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMiddleware.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMiddleware.kt index 9fcfaae65..de2a3fa9a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMiddleware.kt @@ -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)) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt index b88f40b5e..7a847c8ec 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -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 = emptyList(), - val searchTermGroups: List = emptyList(), + val searchTermPartition: TabPartition? = null, val normalTabs: List = emptyList(), val privateTabs: List = emptyList(), val syncing: Boolean = false, @@ -142,9 +142,9 @@ sealed class TabsTrayAction : Action { data class UpdateInactiveTabs(val tabs: List) : 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) : 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 -> diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt index 9b5002add..38433efad 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt @@ -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, selectedTabId: String?) { + override fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) { inActiveTabsCount = tabs.size // Early return with an empty list to remove the header/footer items. diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt index 2f675f269..11a74ffb6 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt @@ -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) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt index d7939e690..20cb8b6c2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt @@ -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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt index 5f8bbb083..7303e7bab 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt @@ -71,6 +71,6 @@ class InactiveTabsController( @VisibleForTesting internal fun refreshInactiveTabsSection() { val tabs = tabsTrayStore.state.inactiveTabs - tray.updateTabs(tabs, null) + tray.updateTabs(tabs, null, null) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt index cbe2a5f25..26d50fb34 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt @@ -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(store) { override suspend fun onState(flow: Flow) { - 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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBinding.kt index 43f511b34..c785ebc96 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBinding.kt @@ -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(store) { override suspend fun onState(flow: Flow) { - 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) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt index 34c681e28..f6983100e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt @@ -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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroup.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroup.kt deleted file mode 100644 index 510fcd655..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroup.kt +++ /dev/null @@ -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, - - /** - * The last time tabs in this group was accessed. - */ - val lastAccess: Long -) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt index ce620d959..22f40b1e8 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt @@ -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.toSearchGroups] */ - override fun updateTabs(tabs: List, selectedTabId: String?) = + override fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) = throw UnsupportedOperationException("Use submitList instead.") private object DiffCallback : DiffUtil.ItemCallback() { - 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) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupBinding.kt index 0a5d330ea..13c9cbb77 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupBinding.kt @@ -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) -> Unit ) : AbstractBinding(store) { override suspend fun onState(flow: Flow) { - flow.map { it.searchTermGroups } + flow.map { it.searchTermPartition?.tabGroups ?: emptyList() } .ifChanged() .collect { - tray.invoke(it) + tray.invoke(it.filter { tabGroup -> tabGroup.tabIds.isNotEmpty() }) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt index 8abc28355..a6232b5c6 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt @@ -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) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt index 1a72abec9..1947ecfd7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt @@ -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() - override fun updateTabs(tabs: List, selectedTabId: String?) { + override fun updateTabs(tabs: List, 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)) - groupsSet.clear() - groupsSet.addAll(groups.map { it.searchTerm }) - tabsTrayStore?.dispatch(TabsTrayAction.UpdateSearchGroupTabs(groups)) + // Search term tabs + tabsTrayStore?.dispatch(TabsTrayAction.UpdateTabPartitions(minTabPartition)) - // Normal tabs. - val totalNormalTabs = (normalTabs + remainderTabs) - tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(totalNormalTabs)) + groupsSet.clear() + groupsSet.addAll(tabGroups.map { it.id }) } } @@ -64,18 +64,14 @@ private fun List.getInactiveTabs(settings: Settings): List.getSearchGroupTabs(settings: Settings): List { - 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): List { + return if (settings.searchTermTabGroupsAreEnabled) { + tabGroups.filter { + it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE || groupsSet.contains(it.id) + } + } else { + emptyList() } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt index 7e99beafa..31db57014 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt @@ -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( protected var styling: TabsTrayStyling = TabsTrayStyling() @CallSuper - override fun updateTabs(tabs: List, selectedTabId: String?) { + override fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) { this.selectedTabId = selectedTabId submitList(tabs) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt index 86eba261e..f34848104 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt @@ -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 { + 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? { + return tabPartitions[SEARCH_TERM_TAB_GROUPS]?.tabGroups + ?.filter { it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt index b653cc644..089d10fd5 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt @@ -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. */ @@ -49,15 +41,15 @@ internal fun TabSessionState.isNormalTabWithSearchTerm(): Boolean { } /** - * Returns true if the [TabSessionState] has a search term but may or may not be active. + * Returns true if the [TabSessionState] is considered active based on the [maxActiveTime]. */ -internal fun TabSessionState.isNormalTabWithoutSearchTerm(): Boolean { - return !hasSearchTerm() && !content.private +internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean { + return !isActive(maxActiveTime) && !content.private } /** - * Returns true if the [TabSessionState] is considered active based on the [maxActiveTime]. + * Returns true if the [TabSessionState] is not private. */ -internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean { - return !isActive(maxActiveTime) && !content.private +internal fun TabSessionState.isNormalTab(): Boolean { + return !content.private } diff --git a/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt b/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt index e4fe509df..67e1e5587 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt @@ -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 diff --git a/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt b/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt index a3cb9242e..5fd0aaf1d 100644 --- a/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt @@ -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) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddlewareTest.kt new file mode 100644 index 000000000..02124c0f2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/SearchTermTabGroupMiddlewareTest.kt @@ -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 = 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 = 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 = 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")) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayMiddlewareTest.kt index a024729fd..b5cd96b5e 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayMiddlewareTest.kt @@ -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 { - 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 { - 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)) } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt index 30d554794..336d4f623 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt @@ -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" ) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt index 1a4fdd508..ecea67625 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt @@ -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) } } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt index f09ecb77e..1c1b9ddbd 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt @@ -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) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/NormalTabsBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/NormalTabsBindingTest.kt index d623b171b..c49614d99 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/NormalTabsBindingTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/NormalTabsBindingTest.kt @@ -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") } } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBindingTest.kt index 3c18aad7c..53417a76f 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBindingTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/OtherHeaderBindingTest.kt @@ -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) } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBindingTest.kt index c2dc54956..0dc2fbce0 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBindingTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBindingTest.kt @@ -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") } } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabGroupBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabGroupBindingTest.kt index 18b18ca94..4a84699c6 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabGroupBindingTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabGroupBindingTest.kt @@ -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(), 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(), captured) } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt index c3be18796..446bfddef 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt @@ -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) } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt index 0410d7d0f..8fa87059d 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt @@ -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) diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index d55580493..70b179cdc 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -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" }