Close #21451: Add active search term tab groups on home

This commit is contained in:
Roger Yang 2021-09-22 15:16:50 -04:00 committed by mergify[bot]
parent f15291757b
commit 8a15e8a681
18 changed files with 588 additions and 92 deletions

View File

@ -9,24 +9,34 @@ import mozilla.components.browser.state.selector.selectedNormalTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tabs.ext.hasMediaPlayed
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.tabstray.browser.TabGroup
import org.mozilla.fenix.tabstray.browser.maxActiveTime
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import kotlin.math.max
/**
* Get the last opened normal tab and the last tab with in progress media, if available.
* Get the last opened normal tab, last tab with in progress media and last search term group, if available.
*
* @return A list of the last opened tab and the last tab with in progress media
* @return A list of the last opened tab, last tab with in progress media and last search term group
* if distinct and available or an empty list.
*/
fun BrowserState.asRecentTabs(): List<TabSessionState> {
return mutableListOf<TabSessionState>().apply {
fun BrowserState.asRecentTabs(): List<RecentTab> {
return mutableListOf<RecentTab>().apply {
val lastOpenedNormalTab = lastOpenedNormalTab
val inProgressMediaTab = inProgressMediaTab
lastOpenedNormalTab?.let { add(it) }
lastOpenedNormalTab?.let { add(RecentTab.Tab(it)) }
if (inProgressMediaTab == lastOpenedNormalTab) {
secondToLastOpenedNormalTab?.let { add(it) }
secondToLastOpenedNormalTab?.let { add(RecentTab.Tab(it)) }
} else {
inProgressMediaTab?.let { add(it) }
inProgressMediaTab?.let { add(RecentTab.Tab(it)) }
}
if (FeatureFlags.tabGroupFeature) {
lastSearchGroup?.let { add(it) }
}
}
}
@ -54,3 +64,48 @@ val BrowserState.inProgressMediaTab: TabSessionState?
get() = normalTabs
.filter { it.hasMediaPlayed() }
.maxByOrNull { it.lastMediaAccessState.lastMediaAccess }
/**
* Get the most recent search term group.
*/
val BrowserState.lastSearchGroup: RecentTab.SearchGroup?
get() {
val tabGroup = normalTabs.toSearchGroup().lastOrNull() ?: return null
val firstTab = tabGroup.tabs.firstOrNull() ?: return null
return RecentTab.SearchGroup(
tabGroup.searchTerm,
firstTab.id,
firstTab.content.url,
firstTab.content.thumbnail,
tabGroup.tabs.count()
)
}
/**
* Get search term groups sorted by last access time.
*/
private fun List<TabSessionState>.toSearchGroup(): List<TabGroup> {
val data = filter {
it.isNormalTabActiveWithSearchTerm(maxActiveTime)
}.groupBy {
when {
it.content.searchTerms.isNotBlank() -> it.content.searchTerms
else -> it.historyMetadata?.searchTerm ?: ""
}.lowercase()
}
return 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
)
}.sortedBy { it.lastAccess }
}

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.home
import android.graphics.Bitmap
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
@ -17,6 +16,7 @@ import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory
@ -50,7 +50,7 @@ data class Tab(
* @property tip The current [Tip] to show on the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
* @property showSetAsDefaultBrowserCard If true, shows the default browser card
* @property recentTabs The list of recent [TabSessionState] in the [HomeFragment].
* @property recentTabs The list of recent [RecentTab] in the [HomeFragment].
* @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment].
* @property historyMetadata The list of [HistoryMetadataGroup].
* @property pocketArticles The list of [PocketRecommendedStory].
@ -63,7 +63,7 @@ data class HomeFragmentState(
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean = false,
val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<TabSessionState> = emptyList(),
val recentTabs: List<RecentTab> = emptyList(),
val recentBookmarks: List<BookmarkNode> = emptyList(),
val historyMetadata: List<HistoryMetadataGroup> = emptyList(),
val pocketStories: List<PocketRecommendedStory> = emptyList(),
@ -77,7 +77,7 @@ sealed class HomeFragmentAction : Action {
val collections: List<TabCollection>,
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean,
val recentTabs: List<TabSessionState>,
val recentTabs: List<RecentTab>,
val recentBookmarks: List<BookmarkNode>,
val historyMetadata: List<HistoryMetadataGroup>
) :
@ -90,7 +90,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<TabSessionState>) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<RecentTab>) : HomeFragmentAction()
data class RecentBookmarksChange(val recentBookmarks: List<BookmarkNode>) : HomeFragmentAction()
data class HistoryMetadataChange(val historyMetadata: List<HistoryMetadataGroup>) : HomeFragmentAction()
data class SelectPocketStoriesCategory(val categoryName: String) : HomeFragmentAction()

View File

@ -4,16 +4,17 @@
package org.mozilla.fenix.home.recenttabs
import android.graphics.Bitmap
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.selector.normalTabs
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
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.ext.asRecentTabs
import org.mozilla.fenix.ext.lastOpenedNormalTab
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
@ -28,20 +29,39 @@ class RecentTabsListFeature(
) : AbstractBinding<BrowserState>(browserStore) {
override suspend fun onState(flow: Flow<BrowserState>) {
// Listen for changes regarding the currently selected tab, in progress media tab
// and search term groups.
flow
// Listen for changes regarding the currently selected tab and the in progress media tab
// and also for changes (close, undo) in normal tabs that could involve these.
.ifAnyChanged {
val lastOpenedNormalTab = it.lastOpenedNormalTab
arrayOf(
lastOpenedNormalTab?.id,
lastOpenedNormalTab?.content?.title,
lastOpenedNormalTab?.content?.icon,
it.normalTabs
)
}
.map { it.asRecentTabs() }
.ifChanged()
.collect {
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(browserStore.state.asRecentTabs()))
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(it))
}
}
}
sealed class RecentTab {
/**
* A tab that was recently viewed
*
* @param state Recently viewed [TabSessionState]
*/
data class Tab(val state: TabSessionState) : RecentTab()
/**
* A search term group that was recently viewed
*
* @param searchTerm The search term that was recently viewed
* @param tabId The id of the tab that was recently viewed
* @param url The url that was recently viewed
* @param thumbnail The thumbnail of the search term that was recently viewed
* @param count The number of tabs in the search term group
*/
data class SearchGroup(
val searchTerm: String,
val tabId: String,
val url: String,
val thumbnail: Bitmap?,
val count: Int
) : RecentTab()
}

View File

@ -26,6 +26,11 @@ interface RecentTabController {
*/
fun handleRecentTabClicked(tabId: String)
/**
* @see [RecentTabInteractor.onRecentSearchGroupClicked]
*/
fun handleRecentSearchGroupClicked(tabId: String)
/**
* @see [RecentTabInteractor.onRecentTabShowAllClicked]
*/
@ -62,6 +67,11 @@ class DefaultRecentTabsController(
navController.navigate(HomeFragmentDirections.actionGlobalTabsTrayFragment())
}
override fun handleRecentSearchGroupClicked(tabId: String) {
selectTabUseCase.invoke(tabId)
navController.navigate(HomeFragmentDirections.actionGlobalTabsTrayFragment())
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {

View File

@ -15,6 +15,13 @@ interface RecentTabInteractor {
*/
fun onRecentTabClicked(tabId: String)
/**
* Opens the tabs tray and scroll to the search group. Called when a user clicks on a search group.
*
* @param tabId The ID of the tab to open.
*/
fun onRecentSearchGroupClicked(tabId: String)
/**
* Show the tabs tray. Called when a user clicks on the "Show all" button besides the recent
* tabs.

View File

@ -36,7 +36,8 @@ class RecentTabViewHolder(
FirefoxTheme {
RecentTabs(
recentTabs = recentTabs.value ?: emptyList(),
onRecentTabClick = { interactor.onRecentTabClicked(it) }
onRecentTabClick = { interactor.onRecentTabClicked(it) },
onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) }
)
}
}

View File

@ -24,47 +24,69 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.theme.FirefoxTheme
/**
* A list of recent tabs to jump back to.
*
* @param recentTabs List of [TabSessionState] to display.
* @param recentTabs List of [RecentTab] to display.
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
* @param onRecentSearchGroupClicked Invoked when the user clicks on a recent search group.
*/
@Composable
fun RecentTabs(
recentTabs: List<TabSessionState>,
onRecentTabClick: (String) -> Unit = {}
recentTabs: List<RecentTab>,
onRecentTabClick: (String) -> Unit = {},
onRecentSearchGroupClicked: (String) -> Unit = {}
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
recentTabs.forEach { tab ->
RecentTabItem(
tabId = tab.id,
url = tab.content.url,
title = tab.content.title,
icon = tab.content.icon,
onRecentTabClick = onRecentTabClick
)
when (tab) {
is RecentTab.Tab -> {
RecentTabItem(
tabId = tab.state.id,
url = tab.state.content.url,
title = tab.state.content.title,
thumbnail = tab.state.content.thumbnail,
onRecentTabClick = onRecentTabClick
)
}
is RecentTab.SearchGroup -> {
RecentSearchGroupItem(
searchTerm = tab.searchTerm,
tabId = tab.tabId,
url = tab.url,
thumbnail = tab.thumbnail,
count = tab.count,
onSearchGroupClicked = onRecentSearchGroupClicked
)
}
}
}
}
}
@ -72,18 +94,20 @@ fun RecentTabs(
/**
* A recent tab item.
*
* @param tabId Tbe id of the tab.
* @param tabId The id of the tab.
* @param url The loaded URL of the tab.
* @param title The title of the tab.
* @param icon The icon of the tab.
* @param thumbnail The icon of the tab.
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
*/
@Suppress("LongParameterList")
@Composable
private fun RecentTabItem(
tabId: String,
url: String,
title: String,
icon: Bitmap? = null,
thumbnail: Bitmap? = null,
onRecentTabClick: (String) -> Unit = {}
) {
Card(
@ -101,7 +125,9 @@ private fun RecentTabItem(
RecentTabImage(
url = url,
modifier = Modifier.size(116.dp, 84.dp),
icon = icon
icon = thumbnail,
contentScale = ContentScale.FillWidth,
alignment = Alignment.TopCenter
)
Spacer(modifier = Modifier.width(16.dp))
@ -112,7 +138,82 @@ private fun RecentTabItem(
) {
RecentTabTitle(title = title)
RecentTabSubtitle(url = url)
RecentTabSubtitle(subtitle = url)
Row {
RecentTabImage(
url = url,
modifier = Modifier.size(18.dp, 18.dp),
icon = icon
)
Spacer(modifier = Modifier.width(8.dp))
RecentTabSubtitle(subtitle = url)
}
}
}
}
}
/**
* A recent search group item.
*
* @param searchTerm The search term for the group.
* @param tabId The id of the last accessed tab in the group.
* @param url The loaded URL of the last accessed tab in the group.
* @param thumbnail The icon of the group.
* @param count Count of how many tabs belongs to the group.
* @param onSearchGroupClicked Invoked when the user clicks on a group.
*/
@Suppress("LongParameterList")
@Composable
private fun RecentSearchGroupItem(
searchTerm: String,
tabId: String,
url: String,
thumbnail: Bitmap?,
count: Int,
onSearchGroupClicked: (String) -> Unit = {}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(116.dp)
.clickable { onSearchGroupClicked(tabId) },
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.surface,
elevation = 6.dp
) {
Row(
modifier = Modifier.padding(16.dp)
) {
RecentTabImage(
url = url,
modifier = Modifier.size(116.dp, 84.dp),
icon = thumbnail,
contentScale = ContentScale.FillWidth,
alignment = Alignment.TopCenter
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
RecentTabTitle(title = stringResource(R.string.recent_tabs_search_term, searchTerm))
Row {
Icon(
painter = painterResource(id = R.drawable.ic_all_tabs),
contentDescription = null // decorative element
)
Spacer(modifier = Modifier.width(8.dp))
RecentTabSubtitle(subtitle = stringResource(R.string.recent_tabs_search_term_count, count))
}
}
}
}
@ -130,6 +231,8 @@ private fun RecentTabItem(
private fun RecentTabImage(
url: String,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
alignment: Alignment = Alignment.Center,
icon: Bitmap? = null
) {
if (icon != null) {
@ -137,6 +240,8 @@ private fun RecentTabImage(
painter = BitmapPainter(icon.asImageBitmap()),
contentDescription = null,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
} else {
components.core.icons.Loader(
@ -157,7 +262,7 @@ private fun RecentTabImage(
Image(
painter = icon.painter,
contentDescription = null,
modifier = modifier,
modifier = modifier
)
}
}
@ -183,12 +288,12 @@ private fun RecentTabTitle(title: String) {
/**
* A recent tab subtitle.
*
* @param url The loaded URL of the tab.
* @param subtitle The loaded URL of the tab.
*/
@Composable
private fun RecentTabSubtitle(url: String) {
private fun RecentTabSubtitle(subtitle: String) {
Text(
text = url.getRepresentativeSnippet(),
text = subtitle.getRepresentativeSnippet(),
color = FirefoxTheme.colors.textSecondary,
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,

View File

@ -355,6 +355,10 @@ class SessionControlInteractor(
recentTabController.handleRecentTabClicked(tabId)
}
override fun onRecentSearchGroupClicked(tabId: String) {
recentTabController.handleRecentSearchGroupClicked(tabId)
}
override fun onRecentTabShowAllClicked() {
recentTabController.handleRecentTabShowAllClicked()
}

View File

@ -11,7 +11,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
@ -25,6 +24,7 @@ import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.utils.Settings
// This method got a little complex with the addition of the tab tray feature flag
@ -40,7 +40,7 @@ internal fun normalModeAdapterItems(
recentBookmarks: List<BookmarkNode>,
showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean,
recentTabs: List<TabSessionState>,
recentTabs: List<RecentTab>,
historyMetadata: List<HistoryMetadataGroup>,
pocketStories: List<PocketRecommendedStory>
): List<AdapterItem> {

View File

@ -0,0 +1,24 @@
/* 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.
*/
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
)

View File

@ -20,7 +20,6 @@ import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.TabGroupAdapter.Group
import kotlin.math.max
import mozilla.components.concept.tabstray.Tab as TabsTrayTab
import mozilla.components.support.base.observer.Observable
@ -42,8 +41,10 @@ class TabGroupAdapter(
private val store: TabsTrayStore,
private val featureName: String,
delegate: TrayObservable = ObserverRegistry()
) : ListAdapter<Group, TabGroupViewHolder>(DiffCallback), TabsTray, TrayObservable by delegate {
) : ListAdapter<TabGroupAdapter.Group, TabGroupViewHolder>(DiffCallback), TabsTray, TrayObservable by delegate {
// TODO use [List<TabSessionState>.toSearchGroup()]
// see https://github.com/mozilla-mobile/android-components/issues/11012
data class Group(
/**
* A title for the tab group.
@ -138,6 +139,6 @@ class TabGroupAdapter(
}
}
internal fun Group.containsTabId(tabId: String): Boolean {
internal fun TabGroupAdapter.Group.containsTabId(tabId: String): Boolean {
return tabs.firstOrNull { it.id == tabId } != null
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M0,2.5C0,1.119 1.119,0 2.5,0H15.5C16.881,0 18,1.119 18,2.5V10.5C18,11.881 16.881,13 15.5,13H2.5C1.119,13 0,11.881 0,10.5V2.5ZM15.7,11.5L16.5,10.7V2.3L15.7,1.5H2.3L1.5,2.3V10.7L2.3,11.5H15.7ZM1.5,15.7L2.3,16.5H15.7L16.5,15.7V14.75C16.5,14.336 16.836,14 17.25,14C17.664,14 18,14.336 18,14.75V15.5C18,16.881 16.881,18 15.5,18H2.5C1.119,18 0,16.881 0,15.5V14.75C0,14.336 0.336,14 0.75,14C1.164,14 1.5,14.336 1.5,14.75V15.7Z"
android:fillColor="#15141A"
android:fillType="evenOdd"/>
</vector>

View File

@ -118,6 +118,10 @@
<string name="recent_tabs_header">Jump back in</string>
<!-- Button text for showing all the tabs in the tabs tray -->
<string name="recent_tabs_show_all">Show all</string>
<!-- Title for showing a group item in the 'Jump back in' section of the new tab -->
<string name="recent_tabs_search_term">Your search for \"%1$s\"</string>
<!-- Text for the number of tabs in a group in the 'Jump back in' section of the new tab -->
<string name="recent_tabs_search_term_count">%1$s sites</string>
<!-- History Metadata -->
<!-- Header text for a section on the home screen that displays grouped highlights from the

View File

@ -8,9 +8,11 @@ import io.mockk.mockk
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.LastMediaAccessState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.storage.HistoryMetadataKey
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.mozilla.fenix.home.recenttabs.RecentTab
class BrowserStateTest {
@ -47,7 +49,7 @@ class BrowserStateTest {
val result = browserState.asRecentTabs()
assertEquals(1, result.size)
assertEquals(selectedTab, result[0])
assertEquals(selectedTab, (result[0] as RecentTab.Tab).state)
}
@Test
@ -66,7 +68,7 @@ class BrowserStateTest {
val result = browserState.asRecentTabs()
assertEquals(1, result.size)
assertEquals(lastAccessedNormalTab, result[0])
assertEquals(lastAccessedNormalTab, (result[0] as RecentTab.Tab).state)
}
@Test
@ -84,8 +86,8 @@ class BrowserStateTest {
val result = browserState.asRecentTabs()
assertEquals(2, result.size)
assertEquals(selectedTab, result[0])
assertEquals(mediaTab, result[1])
assertEquals(selectedTab, (result[0] as RecentTab.Tab).state)
assertEquals(mediaTab, (result[1] as RecentTab.Tab).state)
}
@Test
@ -109,8 +111,8 @@ class BrowserStateTest {
val result = browserState.asRecentTabs()
assertEquals(2, result.size)
assertEquals(lastAccessedNormalTab, result[0])
assertEquals(mediaTab, result[1])
assertEquals(lastAccessedNormalTab, (result[0] as RecentTab.Tab).state)
assertEquals(mediaTab, (result[1] as RecentTab.Tab).state)
}
@Test
@ -129,7 +131,64 @@ class BrowserStateTest {
val result = browserState.asRecentTabs()
assertEquals(2, result.size)
assertEquals(mediaTab, result[0])
assertEquals(mediaTab, (result[0] as RecentTab.Tab).state)
}
@Test
fun `GIVEN a tab group exists WHEN recentTabs is called THEN return a tab group`() {
val searchGroupTab = createTab(
url = "https://www.mozilla.org",
id = "1",
historyMetadata = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "Test",
referrerUrl = "https://www.mozilla.org"
)
)
val browserState = BrowserState(
tabs = listOf(searchGroupTab),
selectedTabId = searchGroupTab.id
)
val result = browserState.asRecentTabs()
assertEquals(2, result.size)
assertEquals(searchGroupTab, (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(1, (result[1] as RecentTab.SearchGroup).count)
}
@Test
fun `GIVEN the selected tab is a normal tab and tab group exists WHEN asRecentTabs is called THEN return a list of these tabs`() {
val selectedTab = createTab(url = "url", id = "3")
val searchGroupTab = createTab(
url = "https://www.mozilla.org",
id = "4",
historyMetadata = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "Test",
referrerUrl = "https://www.mozilla.org"
)
)
val browserState = BrowserState(
tabs = listOf(mockk(relaxed = true), selectedTab, searchGroupTab),
selectedTabId = selectedTab.id
)
val result = browserState.asRecentTabs()
assertEquals(3, result.size)
assertEquals(selectedTab, (result[0] as RecentTab.Tab).state)
assert(result[2] is RecentTab.SearchGroup)
assertEquals(searchGroupTab.historyMetadata?.searchTerm, (result[2] as RecentTab.SearchGroup).searchTerm)
assertEquals(searchGroupTab.id, (result[2] as RecentTab.SearchGroup).tabId)
assertEquals(searchGroupTab.content.url, (result[2] as RecentTab.SearchGroup).url)
assertEquals(searchGroupTab.content.thumbnail, (result[2] as RecentTab.SearchGroup).thumbnail)
assertEquals(1, (result[2] as RecentTab.SearchGroup).count)
}
@Test

View File

@ -10,7 +10,6 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
@ -27,6 +26,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory
import org.mozilla.fenix.onboarding.FenixOnboarding
@ -111,7 +111,7 @@ class HomeFragmentStoreTest {
assertEquals(0, homeFragmentStore.state.recentTabs.size)
// Add 2 TabSessionState to the HomeFragmentStore.
val recentTabs: List<TabSessionState> = listOf(mockk(), mockk())
val recentTabs: List<RecentTab> = listOf(mockk(), mockk())
homeFragmentStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabs)).join()
assertEquals(recentTabs, homeFragmentStore.state.recentTabs)
@ -163,7 +163,7 @@ class HomeFragmentStoreTest {
val collections: List<TabCollection> = listOf(mockk())
val topSites: List<TopSite> = listOf(mockk(), mockk())
val recentTabs: List<TabSessionState> = listOf(mockk(), mockk())
val recentTabs: List<RecentTab> = listOf(mockk(), mockk())
val recentBookmarks: List<BookmarkNode> = listOf(mockk(), mockk())
val historyMetadata: List<HistoryMetadataGroup> = listOf(mockk(), mockk())

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.home
import android.graphics.Bitmap
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
@ -16,6 +17,7 @@ import mozilla.components.browser.state.state.LastMediaAccessState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.feature.media.middleware.LastMediaAccessMiddleware
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
@ -30,6 +32,7 @@ import org.junit.Before
import org.junit.Rule
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
class RecentTabsListFeatureTest {
@ -137,8 +140,10 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
assertEquals(selectedTab, homeStore.state.recentTabs[0])
assertEquals(mediaTab, homeStore.state.recentTabs[1])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(selectedTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
assertTrue(homeStore.state.recentTabs[1] is RecentTab.Tab)
assertEquals(mediaTab, (homeStore.state.recentTabs[1] as RecentTab.Tab).state)
}
@Test
@ -162,7 +167,8 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(selectedMediaTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(selectedMediaTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
}
@Test
@ -192,14 +198,16 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(tab1, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(tab1, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(tab2, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(tab2, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
}
@Test
@ -227,22 +235,28 @@ class RecentTabsListFeatureTest {
feature.start()
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
assertEquals(initialMediaTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(initialMediaTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
browserStore.dispatch(
MediaSessionAction.UpdateMediaPlaybackStateAction("2", MediaSession.PlaybackState.PLAYING)
).joinBlocking()
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
assertEquals(initialMediaTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(initialMediaTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
// UpdateMediaPlaybackStateAction would set the current timestamp as the new value for lastMediaAccess
val updatedLastMediaAccess = homeStore.state.recentTabs[1].lastMediaAccessState.lastMediaAccess
val updatedLastMediaAccess =
(homeStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaAccess
assertTrue("expected lastMediaAccess ($updatedLastMediaAccess) > 100", updatedLastMediaAccess > 100)
assertEquals("http://mozilla.org", homeStore.state.recentTabs[1].lastMediaAccessState.lastMediaUrl)
assertEquals(
"http://mozilla.org",
(homeStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaUrl
)
// Check that the media tab is updated ignoring just the lastMediaAccess property.
assertEquals(
newMediaTab,
homeStore.state.recentTabs[1].copy(
(homeStore.state.recentTabs[1] as RecentTab.Tab).state.copy(
lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 100)
)
)
@ -282,7 +296,8 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(selectedNormalTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(selectedNormalTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
browserStore.dispatch(TabListAction.SelectTabAction(privateTab.id)).joinBlocking()
@ -290,7 +305,8 @@ class RecentTabsListFeatureTest {
// If the selected tab is a private tab the feature should show the last accessed normal tab.
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(lastAccessedNormalTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(lastAccessedNormalTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
}
@Test
@ -316,9 +332,9 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
middleware.assertLastAction(RecentTabsChange::class) {
val tab = it.recentTabs.first()
assertTrue(tab.content.title.isEmpty())
assertNull(tab.content.icon)
val tab = it.recentTabs.first() as RecentTab.Tab
assertTrue(tab.state.content.title.isEmpty())
assertNull(tab.state.content.icon)
}
browserStore.dispatch(UpdateTitleAction("1", "test")).joinBlocking()
@ -326,9 +342,9 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
middleware.assertLastAction(RecentTabsChange::class) {
val tab = it.recentTabs.first()
assertEquals("test", tab.content.title)
assertNull(tab.content.icon)
val tab = it.recentTabs.first() as RecentTab.Tab
assertEquals("test", tab.state.content.title)
assertNull(tab.state.content.icon)
}
browserStore.dispatch(UpdateIconAction("1", "https://www.mozilla.org", mockk()))
@ -337,9 +353,9 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
middleware.assertLastAction(RecentTabsChange::class) {
val tab = it.recentTabs.first()
assertEquals("test", tab.content.title)
assertNotNull(tab.content.icon)
val tab = it.recentTabs.first() as RecentTab.Tab
assertEquals("test", tab.state.content.title)
assertNotNull(tab.state.content.icon)
}
}
@ -361,6 +377,179 @@ class RecentTabsListFeatureTest {
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(selectedTab, homeStore.state.recentTabs[0])
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(selectedTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
}
@Test
fun `GIVEN a selected tab group WHEN the feature starts THEN dispatch the selected tab group as a recent tab list`() {
val tab = createTab(
url = "https://www.mozilla.org",
id = "1",
historyMetadata = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "test search term",
referrerUrl = "https://www.mozilla.org"
)
)
val tabs = listOf(tab)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "1"
)
)
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
val searchGroup = (homeStore.state.recentTabs[1] as RecentTab.SearchGroup)
assertEquals(searchGroup.searchTerm, "Test search term")
assertEquals(searchGroup.tabId, "1")
assertEquals(searchGroup.url, "https://www.mozilla.org")
assertEquals(searchGroup.thumbnail, null)
assertEquals(searchGroup.count, 1)
}
@Test
fun `GIVEN a selected tab group and a selected tab WHEN the feature starts THEN dispatch both the selected tab and the selected tab group as a recent tab list`() {
val tab1 = createTab(
url = "https://www.mozilla.org",
id = "1"
)
val tab2 = createTab(
url = "https://www.mozilla.org",
id = "2",
historyMetadata = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "test search term",
referrerUrl = "https://www.mozilla.org"
)
)
val tabs = listOf(tab1, tab2)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "1"
)
)
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
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.tabId, "2")
assertEquals(searchGroup.url, "https://www.mozilla.org")
assertEquals(searchGroup.thumbnail, null)
assertEquals(searchGroup.count, 1)
}
@Test
fun `GIVEN a selected tab group with 2 tabs WHEN the feature starts THEN dispatch both tab in the selected tab group in the recent tab list`() {
val tab1 = createTab(
url = "https://www.mozilla.org",
id = "1",
historyMetadata = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "test search term",
referrerUrl = "https://www.mozilla.org"
)
)
val tab2 = createTab(
url = "https://www.getpocket.com",
id = "2",
historyMetadata = HistoryMetadataKey(
url = "https://www.getpocket.com",
searchTerm = "Test Search Term",
referrerUrl = "https://www.getpocket.com"
)
)
val tabs = listOf(tab1, tab2)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "2"
)
)
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(2, homeStore.state.recentTabs.size)
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(tab2, (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.tabId, "1")
assertEquals(searchGroup.url, "https://www.mozilla.org")
assertEquals(searchGroup.thumbnail, null)
assertEquals(searchGroup.count, 2)
}
@Test
fun `GIVEN a valid inProgressMediaTabId, selected tab and tab group exists WHEN the feature starts THEN dispatch all as as a recent tabs list`() {
val mediaTab = createTab(
url = "https://mozilla.com", id = "42",
lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123)
)
val selectedTab = createTab("https://mozilla.com", id = "43")
val historyMetadataKey = HistoryMetadataKey(
url = "https://www.mozilla.org",
searchTerm = "test search term",
referrerUrl = "https://www.mozilla.org"
)
val thumbnail = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val searchTermTab = createTab(
url = "https://www.mozilla.org",
id = "44",
thumbnail = thumbnail,
historyMetadata = historyMetadataKey
)
val browserStore = BrowserStore(
BrowserState(
tabs = listOf(mediaTab, selectedTab, searchTermTab),
selectedTabId = "43"
)
)
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(3, homeStore.state.recentTabs.size)
assertTrue(homeStore.state.recentTabs[0] is RecentTab.Tab)
assertEquals(selectedTab, (homeStore.state.recentTabs[0] as RecentTab.Tab).state)
assertTrue(homeStore.state.recentTabs[1] is RecentTab.Tab)
assertEquals(mediaTab, (homeStore.state.recentTabs[1] as RecentTab.Tab).state)
val searchGroup = (homeStore.state.recentTabs[2] as RecentTab.SearchGroup)
assertEquals(searchGroup.searchTerm, "Test search term")
assertEquals(searchGroup.tabId, "44")
assertEquals(searchGroup.url, "https://www.mozilla.org")
assertEquals(searchGroup.thumbnail, thumbnail)
assertEquals(searchGroup.count, 1)
}
}

View File

@ -158,6 +158,13 @@ class SessionControlInteractorTest {
verify { recentTabController.handleRecentTabClicked(tabId) }
}
@Test
fun onRecentSearchGroupClicked() {
val tabId = "tabId"
interactor.onRecentSearchGroupClicked(tabId)
verify { recentTabController.handleRecentSearchGroupClicked(tabId) }
}
@Test
fun onRecentTabShowAllClicked() {
interactor.onRecentTabShowAllClicked()

View File

@ -9,7 +9,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.feature.tab.collections.TabCollection
@ -24,6 +23,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
@ -44,7 +44,7 @@ class SessionControlViewTest {
@Test
fun `GIVEN recentTabs WHEN calling shouldShowHomeOnboardingDialog THEN show the dialog `() {
val recentTabs = listOf<TabSessionState>(mockk())
val recentTabs = listOf<RecentTab>(mockk())
val settings: Settings = mockk()
every { settings.hasShownHomeOnboardingDialog } returns false
@ -101,7 +101,7 @@ class SessionControlViewTest {
interactor,
mockk(relaxed = true)
)
val recentTabs = listOf<TabSessionState>(mockk(relaxed = true))
val recentTabs = listOf<RecentTab>(mockk(relaxed = true))
val state = HomeFragmentState(recentTabs = recentTabs)
@ -140,7 +140,7 @@ class SessionControlViewTest {
val expandedCollections = emptySet<Long>()
val recentBookmarks =
listOf(BookmarkNode(BookmarkNodeType.ITEM, "guid", null, null, null, null, 0, null))
val recentTabs = emptyList<TabSessionState>()
val recentTabs = emptyList<RecentTab.Tab>()
val historyMetadata = emptyList<HistoryMetadataGroup>()
val pocketArticles = emptyList<PocketRecommendedStory>()
@ -168,7 +168,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<BookmarkNode>()
val recentTabs = listOf<TabSessionState>(mockk())
val recentTabs = listOf<RecentTab.Tab>(mockk())
val historyMetadata = emptyList<HistoryMetadataGroup>()
val pocketArticles = emptyList<PocketRecommendedStory>()
@ -197,7 +197,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<BookmarkNode>()
val recentTabs = emptyList<TabSessionState>()
val recentTabs = emptyList<RecentTab.Tab>()
val historyMetadata = listOf(HistoryMetadataGroup("title", emptyList()))
val pocketArticles = emptyList<PocketRecommendedStory>()
@ -226,7 +226,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<BookmarkNode>()
val recentTabs = emptyList<TabSessionState>()
val recentTabs = emptyList<RecentTab.Tab>()
val historyMetadata = emptyList<HistoryMetadataGroup>()
val pocketArticles = listOf(PocketRecommendedStory("", "", "", "", 0, ""))
val context = spyk(testContext)
@ -259,7 +259,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<BookmarkNode>()
val recentTabs = emptyList<TabSessionState>()
val recentTabs = emptyList<RecentTab.Tab>()
val historyMetadata = emptyList<HistoryMetadataGroup>()
val pocketArticles = emptyList<PocketRecommendedStory>()
val context = spyk(testContext)