[fenix] For https://github.com/mozilla-mobile/fenix/issues/26227 - Remove search term tab groups from Recently Visited

pull/600/head
Noah Bond 2 years ago committed by mergify[bot]
parent c98cdcf0b3
commit 35b2c39f0e

@ -60,7 +60,6 @@ sealed class AppAction : Action {
data class RemoveRecentBookmark(val recentBookmark: RecentBookmark) : AppAction() data class RemoveRecentBookmark(val recentBookmark: RecentBookmark) : AppAction()
data class RecentHistoryChange(val recentHistory: List<RecentlyVisitedItem>) : AppAction() data class RecentHistoryChange(val recentHistory: List<RecentlyVisitedItem>) : AppAction()
data class RemoveRecentHistoryHighlight(val highlightUrl: String) : AppAction() data class RemoveRecentHistoryHighlight(val highlightUrl: String) : AppAction()
data class DisbandSearchGroupAction(val searchTerm: String) : AppAction()
/** /**
* Indicates the given [categoryName] was selected by the user. * Indicates the given [categoryName] was selected by the user.
*/ */

@ -4,18 +4,15 @@
package org.mozilla.fenix.components.appstate package org.mozilla.fenix.components.appstate
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.ext.recordNewImpression import mozilla.components.service.pocket.ext.recordNewImpression
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.filterOutTab import org.mozilla.fenix.ext.filterOutTab
import org.mozilla.fenix.ext.getFilteredStories import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.ext.recentSearchGroup
import org.mozilla.fenix.gleanplumb.state.MessagingReducer import org.mozilla.fenix.gleanplumb.state.MessagingReducer
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
/** /**
* Reducer for [AppStore]. * Reducer for [AppStore].
@ -90,14 +87,6 @@ internal object AppStoreReducer {
it is RecentlyVisitedItem.RecentHistoryHighlight && it.url == action.highlightUrl it is RecentlyVisitedItem.RecentHistoryHighlight && it.url == action.highlightUrl
} }
) )
is AppAction.DisbandSearchGroupAction -> state.copy(
recentHistory = state.recentHistory.filterNot {
it is RecentHistoryGroup && (
it.title.equals(action.searchTerm, true) ||
it.title.equals(state.recentSearchGroup?.searchTerm, true)
)
}
)
is AppAction.SelectPocketStoriesCategory -> { is AppAction.SelectPocketStoriesCategory -> {
val updatedCategoriesState = state.copy( val updatedCategoriesState = state.copy(
pocketStoriesCategoriesSelections = pocketStoriesCategoriesSelections =
@ -205,16 +194,3 @@ internal object AppStoreReducer {
) )
} }
} }
/**
* Removes a [RecentHistoryGroup] identified by [groupTitle] if it exists in the current list.
*
* @param groupTitle [RecentHistoryGroup.title] of the item that should be removed.
*/
@VisibleForTesting
internal fun List<RecentlyVisitedItem>.filterOut(groupTitle: String?): List<RecentlyVisitedItem> {
return when (groupTitle != null) {
true -> filterNot { it is RecentHistoryGroup && it.title.equals(groupTitle, true) }
false -> this
}
}

@ -16,7 +16,6 @@ import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketStory import org.mozilla.fenix.home.pocket.PocketStory
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab.SearchGroup
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
/** /**
@ -162,13 +161,6 @@ internal fun getFilteredSponsoredStories(
.toList() .toList()
} }
/**
* Get the [SearchGroup] shown in the "Jump back in" section.
* May be null if no search group is shown.
*/
internal val AppState.recentSearchGroup: SearchGroup?
get() = recentTabs.find { it is SearchGroup } as SearchGroup?
/** /**
* Filter a [AppState] by the blocklist. * Filter a [AppState] by the blocklist.
* *

@ -4,7 +4,6 @@
package org.mozilla.fenix.home.recenttabs package org.mozilla.fenix.home.recenttabs
import android.graphics.Bitmap
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -45,21 +44,4 @@ sealed class RecentTab {
* @param state Recently viewed [TabSessionState] * @param state Recently viewed [TabSessionState]
*/ */
data class Tab(val state: TabSessionState) : RecentTab() data class Tab(val state: TabSessionState) : RecentTab()
/**
* A search term group that was recently viewed
*
* @param searchTerm The search term that was recently viewed. Forced to start with uppercase.
* @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()
} }

@ -81,7 +81,6 @@ fun RecentTabs(
onRecentTabClick = onRecentTabClick onRecentTabClick = onRecentTabClick
) )
} }
else -> {}
} }
} }
} }

@ -17,16 +17,11 @@ import mozilla.components.concept.storage.HistoryHighlightWeights
import mozilla.components.concept.storage.HistoryMetadata import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.HistoryMetadataStorage import mozilla.components.concept.storage.HistoryMetadataStorage
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryGroupInternal
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryHighlightInternal import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryHighlightInternal
import org.mozilla.fenix.utils.Settings.Companion.SEARCH_GROUP_MINIMUM_SITES
import kotlin.math.max
@VisibleForTesting internal const val MAX_RESULTS_TOTAL = 9 @VisibleForTesting internal const val MAX_RESULTS_TOTAL = 9
@VisibleForTesting internal const val MIN_VIEW_TIME_OF_HIGHLIGHT = 10.0 @VisibleForTesting internal const val MIN_VIEW_TIME_OF_HIGHLIGHT = 10.0
@ -50,7 +45,6 @@ class RecentVisitsFeature(
private val historyHighlightsStorage: Lazy<PlacesHistoryStorage>, private val historyHighlightsStorage: Lazy<PlacesHistoryStorage>,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val historyImprovementFeatures: Boolean = FeatureFlags.historyImprovementFeatures,
) : LifecycleAwareFeature { ) : LifecycleAwareFeature {
private var job: Job? = null private var job: Job? = null
@ -69,75 +63,48 @@ class RecentVisitsFeature(
} }
val historyHighlights = getHistoryHighlights(highlights.await(), allHistoryMetadata.await()) val historyHighlights = getHistoryHighlights(highlights.await(), allHistoryMetadata.await())
val historyGroups = getHistorySearchGroups(allHistoryMetadata.await())
updateState(historyHighlights, historyGroups) updateState(historyHighlights)
} }
} }
@VisibleForTesting @VisibleForTesting
internal fun updateState( internal fun updateState(
historyHighlights: List<HistoryHighlightInternal>, historyHighlights: List<HistoryHighlightInternal>,
historyGroups: List<HistoryGroupInternal>
) { ) {
appStore.dispatch( appStore.dispatch(
AppAction.RecentHistoryChange( AppAction.RecentHistoryChange(
getCombinedHistory(historyHighlights, historyGroups) getCombinedHistory(historyHighlights)
) )
) )
} }
/** /**
* Get up to [MAX_RESULTS_TOTAL] items if available as an even split of history highlights and history groups. * Get up to [MAX_RESULTS_TOTAL] items if available of history highlights.
* If more items then needed are available then highlights will be more by one. * Maps the internal highlights and search groups to the final objects to be returned.
* Items will be sorted by their last accessed date so that the most recent will be first.
* *
* @param historyHighlights List of history highlights. Can be empty. * @param historyHighlights List of history highlights. Can be empty.
* @param historyGroups List of history groups. Can be empty.
* *
* @return [RecentlyVisitedItem] list representing the data expected by clients of this feature. * @return [RecentlyVisitedItem] list representing the data expected by clients of this feature.
*/ */
@VisibleForTesting @VisibleForTesting
internal fun getCombinedHistory( internal fun getCombinedHistory(
historyHighlights: List<HistoryHighlightInternal>, historyHighlights: List<HistoryHighlightInternal>,
historyGroups: List<HistoryGroupInternal>
): List<RecentlyVisitedItem> { ): List<RecentlyVisitedItem> {
// Cleanup highlights now to avoid counting them below and then removing the ones found in groups. return historyHighlights
val distinctHighlights = historyHighlights .sortedByDescending { it.lastAccessedTime }
.removeHighlightsAlreadyInGroups(historyGroups) .take(MAX_RESULTS_TOTAL)
.map {
val totalItemsCount = distinctHighlights.size + historyGroups.size RecentHistoryHighlight(
title = if (it.historyHighlight.title.isNullOrBlank()) {
return if (totalItemsCount <= MAX_RESULTS_TOTAL) { it.historyHighlight.url
getSortedHistory( } else {
distinctHighlights.sortedByDescending { it.lastAccessedTime }, it.historyHighlight.title!!
historyGroups.sortedByDescending { it.lastAccessedTime } },
) url = it.historyHighlight.url
} else { )
var groupsCount = 0
var highlightCount = 0
while ((highlightCount + groupsCount) < MAX_RESULTS_TOTAL) {
if ((highlightCount + groupsCount) < MAX_RESULTS_TOTAL &&
distinctHighlights.getOrNull(highlightCount) != null
) {
highlightCount += 1
}
if ((highlightCount + groupsCount) < MAX_RESULTS_TOTAL &&
historyGroups.getOrNull(groupsCount) != null
) {
groupsCount += 1
}
} }
getSortedHistory(
distinctHighlights
.sortedByDescending { it.lastAccessedTime }
.take(highlightCount),
historyGroups
.sortedByDescending { it.lastAccessedTime }
.take(groupsCount)
)
}
} }
/** /**
@ -175,101 +142,11 @@ class RecentVisitsFeature(
} }
} }
/**
* Group all urls accessed following a particular search.
* Automatically dedupes identical urls and adds each url's view time to the group's total.
*
* @param metadata List of history visits.
*
* @return List of user searches and all urls accessed from those.
*/
@VisibleForTesting
internal fun getHistorySearchGroups(
metadata: List<HistoryMetadata>
): List<HistoryGroupInternal> {
return metadata
.filter { it.totalViewTime > 0 && it.key.searchTerm != null }
.groupBy { it.key.searchTerm!! }
.mapValues { group ->
// Within a group, we dedupe entries based on their url so we don't display
// a page multiple times in the same group, and we sum up the total view time
// of deduped entries while making sure to keep the latest updatedAt value.
val metadataInGroup = group.value
val metadataUrlGroups = metadataInGroup.groupBy { metadata -> metadata.key.url }
metadataUrlGroups.map { metadata ->
metadata.value.reduce { acc, elem ->
acc.copy(
totalViewTime = acc.totalViewTime + elem.totalViewTime,
updatedAt = max(acc.updatedAt, elem.updatedAt)
)
}
}
}
.map {
HistoryGroupInternal(
groupName = it.key,
groupItems = it.value
)
}
.filter {
if (historyImprovementFeatures) {
it.groupItems.size >= SEARCH_GROUP_MINIMUM_SITES
} else {
true
}
}
}
/**
* Maps the internal highlights and search groups to the final objects to be returned.
* Items will be sorted by their last accessed date so that the most recent will be first.
*/
@VisibleForTesting
internal fun getSortedHistory(
historyHighlights: List<HistoryHighlightInternal>,
historyGroups: List<HistoryGroupInternal>
): List<RecentlyVisitedItem> {
return (historyHighlights + historyGroups)
.sortedByDescending { it.lastAccessedTime }
.map {
when (it) {
is HistoryHighlightInternal -> RecentHistoryHighlight(
title = if (it.historyHighlight.title.isNullOrBlank()) {
it.historyHighlight.url
} else {
it.historyHighlight.title!!
},
url = it.historyHighlight.url
)
is HistoryGroupInternal -> RecentHistoryGroup(
title = it.groupName,
historyMetadata = it.groupItems
)
}
}
}
override fun stop() { override fun stop() {
job?.cancel() job?.cancel()
} }
} }
/**
* Filter out highlights that are already part of a history group.
*/
@VisibleForTesting
internal fun List<HistoryHighlightInternal>.removeHighlightsAlreadyInGroups(
historyMetadata: List<HistoryGroupInternal>
): List<HistoryHighlightInternal> {
return filterNot { highlight ->
historyMetadata.any {
it.groupItems.any {
it.key.url == highlight.historyHighlight.url
}
}
}
}
@VisibleForTesting @VisibleForTesting
internal sealed class RecentlyVisitedItemInternal { internal sealed class RecentlyVisitedItemInternal {
abstract val lastAccessedTime: Long abstract val lastAccessedTime: Long
@ -281,13 +158,4 @@ internal sealed class RecentlyVisitedItemInternal {
val historyHighlight: HistoryHighlight, val historyHighlight: HistoryHighlight,
override val lastAccessedTime: Long override val lastAccessedTime: Long
) : RecentlyVisitedItemInternal() ) : RecentlyVisitedItemInternal()
/**
* Temporary search group allowing for easier data manipulation.
*/
data class HistoryGroupInternal(
val groupName: String,
val groupItems: List<HistoryMetadata>,
override val lastAccessedTime: Long = groupItems.maxOf { it.updatedAt }
) : RecentlyVisitedItemInternal()
} }

@ -4,11 +4,8 @@
package org.mozilla.fenix.home.recentvisits package org.mozilla.fenix.home.recentvisits
import mozilla.components.concept.storage.HistoryMetadata
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
/** /**
* History items as individual or groups of previously accessed webpages. * History items of previously accessed webpages.
*/ */
sealed class RecentlyVisitedItem { sealed class RecentlyVisitedItem {
/** /**
@ -21,18 +18,4 @@ sealed class RecentlyVisitedItem {
val title: String, val title: String,
val url: String val url: String
) : RecentlyVisitedItem() ) : RecentlyVisitedItem()
/**
* A group of previously accessed webpages related by their search terms.
*
* @property title The title of the group.
* @property historyMetadata A list of [HistoryMetadata] records that matches the title.
*/
data class RecentHistoryGroup(
val title: String,
val historyMetadata: List<HistoryMetadata> = emptyList()
) : RecentlyVisitedItem()
} }
// The last updated time of the group is based on the most recently updated item in the group
fun RecentHistoryGroup.lastUpdated(): Long = historyMetadata.maxOf { it.updatedAt }

@ -9,19 +9,14 @@ import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.HistoryMetadataAction
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.HistoryMetadataStorage import mozilla.components.concept.storage.HistoryMetadataStorage
import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.RecentSearches
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.library.history.toHistoryMetadata
/** /**
* All possible updates following user interactions with the "Recent visits" section from the Home screen. * All possible updates following user interactions with the "Recent visits" section from the Home screen.
@ -33,20 +28,6 @@ interface RecentVisitsController {
*/ */
fun handleHistoryShowAllClicked() fun handleHistoryShowAllClicked()
/**
* Callback for when the user clicks on a specific [RecentHistoryGroup].
*
* @param recentHistoryGroup The just clicked [RecentHistoryGroup].
*/
fun handleRecentHistoryGroupClicked(recentHistoryGroup: RecentHistoryGroup)
/**
* Callback for when the user removes a certain [RecentHistoryGroup].
*
* @param groupTitle Title of the [RecentHistoryGroup] to remove.
*/
fun handleRemoveRecentHistoryGroup(groupTitle: String)
/** /**
* Callback for when the user clicks on a specific [RecentHistoryHighlight]. * Callback for when the user clicks on a specific [RecentHistoryHighlight].
* *
@ -84,39 +65,6 @@ class DefaultRecentVisitsController(
) )
} }
/**
* Navigates to the history metadata group fragment to display the group.
*
* @param recentHistoryGroup The [RecentHistoryGroup] to which to navigate to.
*/
override fun handleRecentHistoryGroupClicked(recentHistoryGroup: RecentHistoryGroup) {
navController.navigate(
HomeFragmentDirections.actionGlobalHistoryMetadataGroup(
title = recentHistoryGroup.title,
historyMetadataItems = recentHistoryGroup.historyMetadata
.mapIndexed { index, item -> item.toHistoryMetadata(index) }.toTypedArray()
)
)
}
/**
* Removes a [RecentHistoryGroup] with the given title from the homescreen.
*
* @param groupTitle The title of the [RecentHistoryGroup] to be removed.
*/
override fun handleRemoveRecentHistoryGroup(groupTitle: String) {
// We want to update the UI right away in response to user action without waiting for the IO.
// First, dispatch actions that will clean up search groups in the two stores that have
// metadata-related state.
store.dispatch(HistoryMetadataAction.DisbandSearchGroupAction(searchTerm = groupTitle))
appStore.dispatch(AppAction.DisbandSearchGroupAction(searchTerm = groupTitle))
// Then, perform the expensive IO work of removing search groups from storage.
scope.launch {
storage.deleteHistoryMetadata(groupTitle)
}
RecentSearches.groupDeleted.record(NoExtras())
}
/** /**
* Switch to an already open tab for [recentHistoryHighlight] if one exists or * Switch to an already open tab for [recentHistoryHighlight] if one exists or
* create a new tab in which to load this item's URL. * create a new tab in which to load this item's URL.

@ -4,7 +4,6 @@
package org.mozilla.fenix.home.recentvisits.interactor package org.mozilla.fenix.home.recentvisits.interactor
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
/** /**
@ -17,20 +16,6 @@ interface RecentVisitsInteractor {
*/ */
fun onHistoryShowAllClicked() fun onHistoryShowAllClicked()
/**
* Callbacks for when the user clicks on a [RecentHistoryGroup].
*
* @param recentHistoryGroup The just clicked [RecentHistoryGroup].
*/
fun onRecentHistoryGroupClicked(recentHistoryGroup: RecentHistoryGroup)
/**
* Callback for when the user selected an option to remove a [RecentHistoryGroup].
*
* @param groupTitle [RecentHistoryGroup.title] of the item to remove.
*/
fun onRemoveRecentHistoryGroup(groupTitle: String)
/** /**
* Callback for when the user clicks on a [RecentHistoryHighlight]. * Callback for when the user clicks on a [RecentHistoryHighlight].
* *

@ -5,10 +5,8 @@
package org.mozilla.fenix.home.recentvisits.view package org.mozilla.fenix.home.recentvisits.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -18,7 +16,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -40,17 +37,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.EagerFlingBehavior import org.mozilla.fenix.compose.EagerFlingBehavior
import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme import org.mozilla.fenix.theme.Theme
@ -103,15 +96,6 @@ fun RecentlyVisited(
onRecentVisitClick(it, pageIndex + 1) onRecentVisitClick(it, pageIndex + 1)
} }
) )
is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
recentVisit = recentVisit,
menuItems = menuItems,
clickableEnabled = listState.atLeastHalfVisibleItems.contains(pageIndex),
showDividerLine = index < items.size - 1,
onRecentVisitClick = {
onRecentVisitClick(it, pageIndex + 1)
}
)
} }
} }
} }
@ -120,73 +104,6 @@ fun RecentlyVisited(
} }
} }
/**
* A recently visited history group.
*
* @param recentVisit The [RecentHistoryGroup] to display.
* @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
* @param clickableEnabled Whether click actions should be invoked or not.
* @param showDividerLine Whether to show a divider line at the bottom.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RecentlyVisitedHistoryGroup(
recentVisit: RecentHistoryGroup,
menuItems: List<RecentVisitMenuItem>,
clickableEnabled: Boolean,
showDividerLine: Boolean,
onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.combinedClickable(
enabled = clickableEnabled,
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = { isMenuExpanded = true }
)
.size(268.dp, 56.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.ic_multiple_tabs),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.fillMaxSize()
) {
RecentlyVisitedTitle(
text = recentVisit.title,
modifier = Modifier
.padding(top = 7.dp, bottom = 2.dp)
.weight(1f)
)
RecentlyVisitedCaption(
count = recentVisit.historyMetadata.size,
modifier = Modifier.weight(1f)
)
if (showDividerLine) {
RecentlyVisitedDivider()
}
}
RecentlyVisitedMenu(
showMenu = isMenuExpanded,
menuItems = menuItems,
recentVisit = recentVisit,
onDismissRequest = { isMenuExpanded = false }
)
}
}
/** /**
* A recently visited history item. * A recently visited history item.
* *
@ -262,36 +179,6 @@ private fun RecentlyVisitedTitle(
) )
} }
/**
* The caption text for a recent visit.
*
* @param count Number of recently visited items to display in the caption.
* @param modifier [Modifier] allowing to perfectly place this.
*/
@Composable
private fun RecentlyVisitedCaption(
count: Int,
modifier: Modifier
) {
val stringId = if (count == 1) {
R.string.history_search_group_site
} else {
R.string.history_search_group_sites
}
Text(
text = String.format(LocalContext.current.getString(stringId), count),
modifier = modifier,
color = when (isSystemInDarkTheme()) {
true -> FirefoxTheme.colors.textPrimary
false -> FirefoxTheme.colors.textSecondary
},
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
/** /**
* Menu shown for a [RecentlyVisitedItem]. * Menu shown for a [RecentlyVisitedItem].
* *
@ -373,10 +260,14 @@ private fun RecentlyVisitedPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme(theme = Theme.getTheme()) {
RecentlyVisited( RecentlyVisited(
recentVisits = listOf( recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"), RecentHistoryHighlight(
RecentHistoryGroup(title = "mozilla"), title = "Google",
RecentHistoryGroup(title = "firefox"), url = "www.google.com",
RecentHistoryGroup(title = "pocket") ),
RecentHistoryHighlight(
title = "Firefox",
url = "www.firefox.com",
),
), ),
menuItems = emptyList() menuItems = emptyList()
) )

@ -11,13 +11,11 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.lib.state.ext.observeAsComposableState
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.RecentlyVisitedHomepage import org.mozilla.fenix.GleanMetrics.RecentlyVisitedHomepage
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor
@ -51,7 +49,6 @@ class RecentlyVisitedViewHolder(
title = stringResource(R.string.recently_visited_menu_item_remove), title = stringResource(R.string.recently_visited_menu_item_remove),
onClick = { visit -> onClick = { visit ->
when (visit) { when (visit) {
is RecentHistoryGroup -> interactor.onRemoveRecentHistoryGroup(visit.title)
is RecentHistoryHighlight -> interactor.onRemoveRecentHistoryHighlight( is RecentHistoryHighlight -> interactor.onRemoveRecentHistoryHighlight(
visit.url visit.url
) )
@ -59,21 +56,12 @@ class RecentlyVisitedViewHolder(
} }
) )
), ),
onRecentVisitClick = { recentlyVisitedItem, pageNumber -> onRecentVisitClick = { recentlyVisitedItem, _ ->
when (recentlyVisitedItem) { when (recentlyVisitedItem) {
is RecentHistoryHighlight -> { is RecentHistoryHighlight -> {
RecentlyVisitedHomepage.historyHighlightOpened.record(NoExtras()) RecentlyVisitedHomepage.historyHighlightOpened.record(NoExtras())
interactor.onRecentHistoryHighlightClicked(recentlyVisitedItem) interactor.onRecentHistoryHighlightClicked(recentlyVisitedItem)
} }
is RecentHistoryGroup -> {
RecentlyVisitedHomepage.searchGroupOpened.record(NoExtras())
History.recentSearchesTapped.record(
History.RecentSearchesTappedExtra(
pageNumber.toString()
)
)
interactor.onRecentHistoryGroupClicked(recentlyVisitedItem)
}
} }
} }
) )

@ -23,7 +23,6 @@ import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInterac
import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor
@ -404,16 +403,6 @@ class SessionControlInteractor(
recentVisitsController.handleHistoryShowAllClicked() recentVisitsController.handleHistoryShowAllClicked()
} }
override fun onRecentHistoryGroupClicked(recentHistoryGroup: RecentHistoryGroup) {
recentVisitsController.handleRecentHistoryGroupClicked(
recentHistoryGroup
)
}
override fun onRemoveRecentHistoryGroup(groupTitle: String) {
recentVisitsController.handleRemoveRecentHistoryGroup(groupTitle)
}
override fun onRecentHistoryHighlightClicked(recentHistoryHighlight: RecentHistoryHighlight) { override fun onRecentHistoryHighlightClicked(recentHistoryHighlight: RecentHistoryHighlight) {
recentVisitsController.handleRecentHistoryHighlightClicked(recentHistoryHighlight) recentVisitsController.handleRecentHistoryHighlightClicked(recentHistoryHighlight)
} }

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<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="?attr/textSecondary"
android:fillType="evenOdd"/>
</vector>

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="80dp"
android:viewportWidth="108"
android:viewportHeight="80">
<group>
<clip-path
android:pathData="M8,0L100,0A8,8 0,0 1,108 8L108,72A8,8 0,0 1,100 80L8,80A8,8 0,0 1,0 72L0,8A8,8 0,0 1,8 0z"/>
<path
android:pathData="M8,0L100,0A8,8 0,0 1,108 8L108,72A8,8 0,0 1,100 80L8,80A8,8 0,0 1,0 72L0,8A8,8 0,0 1,8 0z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="0"
android:startX="108"
android:endY="103.313"
android:endX="31.4721"
android:type="linear">
<item android:offset="0" android:color="#FF9059FF"/>
<item android:offset="1" android:color="#FF0250BB"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M100,80L8,80A8,8 0,0 1,0 72L0,8A8,8 0,0 1,8 0L100,0A8,8 0,0 1,108 8L108,72A8,8 0,0 1,100 80z"
android:strokeAlpha="0.5"
android:fillAlpha="0.5">
<aapt:attr name="android:fillColor">
<gradient
android:startY="80"
android:startX="54"
android:endY="0"
android:endX="54"
android:type="linear">
<item android:offset="0.0104167" android:color="#FF000000"/>
<item android:offset="0.567708" android:color="#FF81535C"/>
<item android:offset="1" android:color="#00C4C4C4"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M60.55,43.687C62.019,41.645 62.888,39.145 62.888,36.444C62.888,29.584 57.306,24 50.444,24C43.582,24 38,29.584 38,36.444C38,43.305 43.582,48.889 50.444,48.889C53.155,48.889 55.663,48.014 57.709,46.535L58.641,46.524L67.725,55.609C67.985,55.868 68.326,56 68.667,56C69.008,56 69.35,55.87 69.609,55.609C70.13,55.088 70.13,54.244 69.609,53.723L60.532,44.644L60.55,43.687ZM50.444,46.222C45.052,46.222 40.667,41.835 40.667,36.444C40.667,31.054 45.052,26.667 50.444,26.667C55.836,26.667 60.221,31.054 60.221,36.444C60.221,41.835 55.836,46.222 50.444,46.222Z"
android:fillColor="#F9F9FB"/>
</group>
</vector>

@ -117,10 +117,10 @@
<string name="recent_tabs_show_all_content_description_2">Show all recent tabs button</string> <string name="recent_tabs_show_all_content_description_2">Show all recent tabs button</string>
<!-- Title for showing a group item in the 'Jump back in' section of the new tab <!-- Title for showing a group item in the 'Jump back in' section of the new tab
The first parameter is the search term that the user used. (for example: your search for "cat")--> The first parameter is the search term that the user used. (for example: your search for "cat")-->
<string name="recent_tabs_search_term">Your search for \"%1$s\"</string> <string name="recent_tabs_search_term" moz:RemovedIn="105" tools:ignore="UnusedResources">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 <!-- Text for the number of tabs in a group in the 'Jump back in' section of the new tab
%d is a placeholder for the number of sites in the group. This number will always be more than one. --> %d is a placeholder for the number of sites in the group. This number will always be more than one. -->
<string name="recent_tabs_search_term_count_2">%d sites</string> <string name="recent_tabs_search_term_count_2" moz:RemovedIn="105" tools:ignore="UnusedResources">%d sites</string>
<!-- Text for button in synced tab card that opens synced tabs tray --> <!-- Text for button in synced tab card that opens synced tabs tray -->
<string name="recent_tabs_see_all_synced_tabs_button_text">See all synced tabs</string> <string name="recent_tabs_see_all_synced_tabs_button_text">See all synced tabs</string>
<!-- Accessibility description for device icon used for recent synced tab --> <!-- Accessibility description for device icon used for recent synced tab -->

@ -31,7 +31,6 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.filterOut
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getFilteredStories import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.home.CurrentMode import org.mozilla.fenix.home.CurrentMode
@ -43,7 +42,6 @@ import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
@ -133,13 +131,10 @@ class AppStoreTest {
@Test @Test
fun `Test changing the recent tabs in AppStore`() = runTest { fun `Test changing the recent tabs in AppStore`() = runTest {
val group1 = RecentHistoryGroup(title = "title1") val highlight = RecentHistoryHighlight(title = "title", "")
val group2 = RecentHistoryGroup(title = "title2")
val group3 = RecentHistoryGroup(title = "title3")
val highlight = RecentHistoryHighlight(title = group2.title, "")
appStore = AppStore( appStore = AppStore(
AppState( AppState(
recentHistory = listOf(group1, group2, group3, highlight) recentHistory = listOf(highlight)
) )
) )
assertEquals(0, appStore.state.recentTabs.size) assertEquals(0, appStore.state.recentTabs.size)
@ -151,7 +146,7 @@ class AppStoreTest {
appStore.dispatch(AppAction.RecentTabsChange(recentTabs)).join() appStore.dispatch(AppAction.RecentTabsChange(recentTabs)).join()
assertEquals(recentTabs, appStore.state.recentTabs) assertEquals(recentTabs, appStore.state.recentTabs)
assertEquals(listOf(group1, group3, highlight), appStore.state.recentHistory) assertEquals(listOf(highlight), appStore.state.recentHistory)
} }
@Test @Test
@ -177,7 +172,7 @@ class AppStoreTest {
fun `Test changing the history metadata in AppStore`() = runTest { fun `Test changing the history metadata in AppStore`() = runTest {
assertEquals(0, appStore.state.recentHistory.size) assertEquals(0, appStore.state.recentHistory.size)
val historyMetadata: List<RecentHistoryGroup> = listOf(mockk(), mockk()) val historyMetadata: List<RecentHistoryHighlight> = listOf(mockk(), mockk())
appStore.dispatch(AppAction.RecentHistoryChange(historyMetadata)).join() appStore.dispatch(AppAction.RecentHistoryChange(historyMetadata)).join()
assertEquals(historyMetadata, appStore.state.recentHistory) assertEquals(historyMetadata, appStore.state.recentHistory)
@ -185,12 +180,10 @@ class AppStoreTest {
@Test @Test
fun `Test removing a history highlight from AppStore`() = runTest { fun `Test removing a history highlight from AppStore`() = runTest {
val g1 = RecentHistoryGroup(title = "group One")
val g2 = RecentHistoryGroup(title = "grup two")
val h1 = RecentHistoryHighlight(title = "highlight One", url = "url1") val h1 = RecentHistoryHighlight(title = "highlight One", url = "url1")
val h2 = RecentHistoryHighlight(title = "highlight two", url = "url2") val h2 = RecentHistoryHighlight(title = "highlight two", url = "url2")
val recentHistoryState = AppState( val recentHistoryState = AppState(
recentHistory = listOf(g1, g2, h1, h2) recentHistory = listOf(h1, h2)
) )
appStore = AppStore(recentHistoryState) appStore = AppStore(recentHistoryState)
@ -202,7 +195,7 @@ class AppStoreTest {
appStore.dispatch(AppAction.RemoveRecentHistoryHighlight(h1.url)).join() appStore.dispatch(AppAction.RemoveRecentHistoryHighlight(h1.url)).join()
assertEquals( assertEquals(
recentHistoryState.copy(recentHistory = listOf(g1, g2, h2)), recentHistoryState.copy(recentHistory = listOf(h2)),
appStore.state appStore.state
) )
} }
@ -241,16 +234,12 @@ class AppStoreTest {
assertEquals(0, appStore.state.recentHistory.size) assertEquals(0, appStore.state.recentHistory.size)
assertEquals(Mode.Normal, appStore.state.mode) assertEquals(Mode.Normal, appStore.state.mode)
val recentGroup = RecentTab.SearchGroup("testSearchTerm", "id", "url", null, 3)
val collections: List<TabCollection> = listOf(mockk()) val collections: List<TabCollection> = listOf(mockk())
val topSites: List<TopSite> = listOf(mockk(), mockk()) val topSites: List<TopSite> = listOf(mockk(), mockk())
val recentTabs: List<RecentTab> = listOf(mockk(), mockk()) val recentTabs: List<RecentTab> = listOf(mockk(), mockk())
val recentBookmarks: List<RecentBookmark> = listOf(mockk(), mockk()) val recentBookmarks: List<RecentBookmark> = listOf(mockk(), mockk())
val group1 = RecentHistoryGroup(title = "test One") val highlight = RecentHistoryHighlight("title", "")
val group2 = RecentHistoryGroup(title = recentGroup.searchTerm.lowercase()) val recentHistory: List<RecentlyVisitedItem> = listOf(highlight)
val group3 = RecentHistoryGroup(title = "test two")
val highlight = RecentHistoryHighlight(group2.title, "")
val recentHistory: List<RecentlyVisitedItem> = listOf(group1, group2, group3, highlight)
appStore.dispatch( appStore.dispatch(
AppAction.Change( AppAction.Change(
@ -268,7 +257,7 @@ class AppStoreTest {
assertEquals(topSites, appStore.state.topSites) assertEquals(topSites, appStore.state.topSites)
assertEquals(recentTabs, appStore.state.recentTabs) assertEquals(recentTabs, appStore.state.recentTabs)
assertEquals(recentBookmarks, appStore.state.recentBookmarks) assertEquals(recentBookmarks, appStore.state.recentBookmarks)
assertEquals(listOf(group1, group3, highlight), appStore.state.recentHistory) assertEquals(listOf(highlight), appStore.state.recentHistory)
assertEquals(Mode.Private, appStore.state.mode) assertEquals(Mode.Private, appStore.state.mode)
} }
@ -480,21 +469,4 @@ class AppStoreTest {
assertSame(firstFilteredStories, appStore.state.pocketStories) assertSame(firstFilteredStories, appStore.state.pocketStories)
} }
} }
@Test
fun `Test filtering out search groups`() {
val group1 = RecentHistoryGroup("title1")
val group2 = RecentHistoryGroup("title2")
val group3 = RecentHistoryGroup("title3")
val highLight1 = RecentHistoryHighlight("title1", "")
val highLight2 = RecentHistoryHighlight("title2", "")
val highLight3 = RecentHistoryHighlight("title3", "")
val recentHistory = listOf(group1, highLight1, group2, highLight2, group3, highLight3)
assertEquals(recentHistory, recentHistory.filterOut(null))
assertEquals(recentHistory, recentHistory.filterOut(""))
assertEquals(recentHistory, recentHistory.filterOut(" "))
assertEquals(recentHistory - group2, recentHistory.filterOut("Title2"))
assertEquals(recentHistory - group3, recentHistory.filterOut("title3"))
}
} }

@ -24,16 +24,13 @@ import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import mozilla.components.support.test.rule.MainCoroutineRule import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryGroupInternal
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryHighlightInternal import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItemInternal.HistoryHighlightInternal
import kotlin.random.Random import kotlin.random.Random
@ -69,10 +66,6 @@ class RecentVisitsFeatureTest {
documentType = DocumentType.Regular, documentType = DocumentType.Regular,
previewImageUrl = null previewImageUrl = null
) )
val recentHistoryGroup = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(historyEntry)
)
val highlightEntry = HistoryHighlight(1.0, 1, "https://firefox.com", "firefox", null) val highlightEntry = HistoryHighlight(1.0, 1, "https://firefox.com", "firefox", null)
val recentHistoryHighlight = RecentHistoryHighlight("firefox", "https://firefox.com") val recentHistoryHighlight = RecentHistoryHighlight("firefox", "https://firefox.com")
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers { coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers {
@ -87,7 +80,7 @@ class RecentVisitsFeatureTest {
startRecentVisitsFeature() startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) { middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(listOf(recentHistoryGroup, recentHistoryHighlight), it.recentHistory) assertEquals(listOf(recentHistoryHighlight), it.recentHistory)
} }
} }
@ -113,200 +106,7 @@ class RecentVisitsFeatureTest {
} }
@Test @Test
fun `GIVEN groups containing history metadata items with the same url WHEN they are added to store THEN entries are deduped`() = fun `GIVEN multiple highlights exist WHEN they are added to store THEN only MAX_RESULTS_TOTAL are sent`() =
runTestOnMain {
val historyEntry1 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null),
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = 1,
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyEntry2 = HistoryMetadata(
key = HistoryMetadataKey("http://firefox.com", "mozilla", null),
title = "firefox",
createdAt = System.currentTimeMillis(),
updatedAt = 2,
totalViewTime = 20,
documentType = DocumentType.Regular,
previewImageUrl = "http://firefox.com/image1"
)
val historyEntry3 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null),
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = 3,
totalViewTime = 30,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val expectedHistoryGroup = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(
// Expected total view time to be summed up for deduped entries
historyEntry1.copy(
totalViewTime = historyEntry1.totalViewTime + historyEntry3.totalViewTime,
updatedAt = historyEntry3.updatedAt
),
historyEntry2
)
)
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers {
listOf(
historyEntry1, historyEntry2, historyEntry3
)
}
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(listOf(expectedHistoryGroup), it.recentHistory)
}
}
@Test
fun `GIVEN different groups containing history metadata items with the same url WHEN they are added to store THEN entries are not deduped`() =
runTestOnMain {
val now = System.currentTimeMillis()
val historyEntry1 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null),
title = "mozilla",
createdAt = now,
updatedAt = now + 3,
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyEntry2 = HistoryMetadata(
key = HistoryMetadataKey("http://firefox.com", "mozilla", null),
title = "firefox",
createdAt = now,
updatedAt = now + 2,
totalViewTime = 20,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyEntry3 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "firefox", null),
title = "mozilla",
createdAt = now,
updatedAt = now + 1,
totalViewTime = 30,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val expectedHistoryGroup1 = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(historyEntry1, historyEntry2)
)
val expectedHistoryGroup2 = RecentHistoryGroup(
title = "firefox",
historyMetadata = listOf(historyEntry3)
)
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers {
listOf(
historyEntry1, historyEntry2, historyEntry3
)
}
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(listOf(expectedHistoryGroup1, expectedHistoryGroup2), it.recentHistory)
}
}
@Test
fun `GIVEN history groups WHEN they are added to store THEN they are sorted descending by last updated timestamp`() =
runTestOnMain {
val now = System.currentTimeMillis()
val historyEntry1 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null),
title = "mozilla",
createdAt = now,
updatedAt = now + 1,
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyEntry2 = HistoryMetadata(
key = HistoryMetadataKey("http://firefox.com", "mozilla", null),
title = "firefox",
createdAt = now,
updatedAt = now + 2,
totalViewTime = 20,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyEntry3 = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "firefox", null),
title = "mozilla",
createdAt = now,
updatedAt = now + 3,
totalViewTime = 30,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val expectedHistoryGroup1 = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(historyEntry1, historyEntry2)
)
val expectedHistoryGroup2 = RecentHistoryGroup(
title = "firefox",
historyMetadata = listOf(historyEntry3)
)
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers {
listOf(
historyEntry1, historyEntry2, historyEntry3
)
}
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(listOf(expectedHistoryGroup2, expectedHistoryGroup1), it.recentHistory)
}
}
@Test
fun `GIVEN multiple groups exist but no highlights WHEN they are added to store THEN only MAX_RESULTS_TOTAL are sent`() =
runTestOnMain {
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val expectedRecentHistoryGroups = visitsFromSearch
// Expect to only have the last accessed 9 groups.
.subList(1, 10)
.toIndividualRecentHistoryGroups()
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers { visitsFromSearch }
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(
// The 9 most recent groups.
expectedRecentHistoryGroups,
it.recentHistory
)
}
}
@Test
fun `GIVEN multiple highlights exist but no history groups WHEN they are added to store THEN only MAX_RESULTS_TOTAL are sent`() =
runTestOnMain { runTestOnMain {
val highlights = getHistoryHighlightsItems(10) val highlights = getHistoryHighlightsItems(10)
val expectedRecentHighlights = highlights val expectedRecentHighlights = highlights
@ -326,70 +126,12 @@ class RecentVisitsFeatureTest {
} }
@Test @Test
fun `GIVEN multiple history highlights and history groups WHEN they are added to store THEN only last accessed are added`() = fun `GIVEN a list of history highlights WHEN updateState is called THEN emit RecentHistoryChange`() {
runTestOnMain { val feature = spyk(RecentVisitsFeature(appStore, mockk(), mockk(), mockk(), mockk()))
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10)
val expectedRecentHistoryGroups = visitsFromSearch
// Expect only 4 groups. Take 5 here for using in the below zip() and be dropped after.
.subList(5, 10)
.toIndividualRecentHistoryGroups()
val expectedRecentHistoryHighlights = directVisits.reversed().toRecentHistoryHighlights()
val expectedItems = expectedRecentHistoryHighlights.zip(expectedRecentHistoryGroups).flatMap {
listOf(it.first, it.second)
}.take(9)
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers { visitsFromSearch + directVisits }
coEvery { historyHightlightsStorage.getHistoryHighlights(any(), any()) }.coAnswers {
directVisits.toHistoryHighlights()
}
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(expectedItems, it.recentHistory)
}
}
@Test
fun `GIVEN history highlights exist as history metadata WHEN they are added to store THEN don't add highlight dupes`() {
// To know if a highlight appears in a search group each visit's url should be checked.
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directDistinctVisits = getDirectVisitsHistoryMetadataItems(10).takeLast(2)
val directDupeVisits = visitsFromSearch.takeLast(2).map {
// Erase the search term for this to not be mapped to a search group.
// The url remains the same as the item from a group so it should be skipped.
it.copy(key = it.key.copy(searchTerm = null))
}
val expectedRecentHistoryGroups = visitsFromSearch
.subList(3, 10)
.toIndividualRecentHistoryGroups()
val expectedRecentHistoryHighlights = directDistinctVisits.reversed().toRecentHistoryHighlights()
val expectedItems = listOf(
expectedRecentHistoryHighlights.first(),
expectedRecentHistoryGroups.first(),
expectedRecentHistoryHighlights[1]
) + expectedRecentHistoryGroups.subList(1, expectedRecentHistoryGroups.size)
coEvery { historyMetadataStorage.getHistoryMetadataSince(any()) }.coAnswers {
visitsFromSearch + directDistinctVisits + directDupeVisits
}
coEvery { historyHightlightsStorage.getHistoryHighlights(any(), any()) }.coAnswers {
directDistinctVisits.toHistoryHighlights() + directDupeVisits.toHistoryHighlights()
}
startRecentVisitsFeature()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
assertEquals(expectedItems, it.recentHistory)
}
}
@Test
fun `GIVEN a list of history highlights and groups WHEN updateState is called THEN emit RecentHistoryChange`() {
val feature = spyk(RecentVisitsFeature(appStore, mockk(), mockk(), mockk(), mockk(), false))
val expected = List<RecentHistoryHighlight>(1) { mockk() } val expected = List<RecentHistoryHighlight>(1) { mockk() }
every { feature.getCombinedHistory(any(), any()) } returns expected every { feature.getCombinedHistory(any()) } returns expected
feature.updateState(emptyList(), emptyList()) feature.updateState(emptyList())
appStore.waitUntilIdle() appStore.waitUntilIdle()
middleware.assertLastAction(AppAction.RecentHistoryChange::class) { middleware.assertLastAction(AppAction.RecentHistoryChange::class) {
@ -397,84 +139,9 @@ class RecentVisitsFeatureTest {
} }
} }
@Test
fun `GIVEN highlights visits exist in search groups WHEN getCombined is called THEN remove the highlights already in groups`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(4)
val directVisits = getDirectVisitsHistoryMetadataItems(4)
val directDupeVisits = getSearchFromHistoryMetadataItems(2).map {
// Erase the search term for this to not be mapped to a search group.
// The url remains the same as the item from a group so it should be skipped.
it.copy(key = it.key.copy(searchTerm = null))
}
val expected = directVisits.reversed().toRecentHistoryHighlights()
.zip(visitsFromSearch.toIndividualRecentHistoryGroups())
.flatMap {
listOf(it.first, it.second)
}
val result = feature.getCombinedHistory(
(directVisits + directDupeVisits).toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
)
assertEquals(expected, result)
}
@Test
fun `GIVEN fewer than needed highlights and search groups WHEN getCombined is called THEN the result is sorted by date`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(4)
val directVisits = getDirectVisitsHistoryMetadataItems(4)
val expected = directVisits.reversed().toRecentHistoryHighlights()
.zip(visitsFromSearch.toIndividualRecentHistoryGroups())
.flatMap {
listOf(it.first, it.second)
}
val result = feature.getCombinedHistory(
directVisits.toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
)
assertEquals(expected, result)
}
@Test
fun `GIVEN more highlights are newer than search groups WHEN getCombined is called THEN then return an even split then sorted by date`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(5)
val directVisits = getDirectVisitsHistoryMetadataItems(14)
val expected = directVisits.takeLast(5).reversed().toRecentHistoryHighlights() +
visitsFromSearch.takeLast(4).toIndividualRecentHistoryGroups()
val result = feature.getCombinedHistory(
directVisits.toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
)
assertEquals(expected, result)
}
@Test
fun `GIVEN more search groups are newer than highlights WHEN getCombined is called THEN then return an even split then sorted by date`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(14)
val directVisits = getDirectVisitsHistoryMetadataItems(5)
val expected = visitsFromSearch.takeLast(4).toIndividualRecentHistoryGroups() +
directVisits.takeLast(5).reversed().toRecentHistoryHighlights()
val result = feature.getCombinedHistory(
directVisits.toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
)
assertEquals(expected, result)
}
@Test @Test
fun `GIVEN all highlights have metadata WHEN getHistoryHighlights is called THEN return a list of highlights with an inferred last access time`() { fun `GIVEN all highlights have metadata WHEN getHistoryHighlights is called THEN return a list of highlights with an inferred last access time`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false) val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk())
val visitsFromSearch = getSearchFromHistoryMetadataItems(10) val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10) val directVisits = getDirectVisitsHistoryMetadataItems(10)
@ -491,7 +158,7 @@ class RecentVisitsFeatureTest {
@Test @Test
fun `GIVEN not all highlights have metadata WHEN getHistoryHighlights is called THEN set 0 for the highlights with not found last access time`() { fun `GIVEN not all highlights have metadata WHEN getHistoryHighlights is called THEN set 0 for the highlights with not found last access time`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false) val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk())
val visitsFromSearch = getSearchFromHistoryMetadataItems(10) val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10) val directVisits = getDirectVisitsHistoryMetadataItems(10)
val highlightsWithUnknownAccessTime = directVisits.toHistoryHighlightsInternal().take(5).map { val highlightsWithUnknownAccessTime = directVisits.toHistoryHighlightsInternal().take(5).map {
@ -512,7 +179,7 @@ class RecentVisitsFeatureTest {
@Test @Test
fun `GIVEN multiple metadata records for the same highlight WHEN getHistoryHighlights is called THEN set the latest access time from multiple available`() { fun `GIVEN multiple metadata records for the same highlight WHEN getHistoryHighlights is called THEN set the latest access time from multiple available`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false) val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk())
val visitsFromSearch = getSearchFromHistoryMetadataItems(10) val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10) val directVisits = getDirectVisitsHistoryMetadataItems(10)
val newerDirectVisits = directVisits.mapIndexed { index, item -> val newerDirectVisits = directVisits.mapIndexed { index, item ->
@ -533,70 +200,8 @@ class RecentVisitsFeatureTest {
} }
@Test @Test
fun `GIVEN multiple metadata entries only for direct accessed pages WHEN getHistorySearchGroups is called THEN return an empty list`() { fun `GIVEN highlights don't have a valid title WHEN getCombinedHistory is called THEN the url is set as title`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false) val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk())
val directVisits = getDirectVisitsHistoryMetadataItems(10)
val result = feature.getHistorySearchGroups(directVisits)
assertTrue(result.isEmpty())
}
@Test
fun `GIVEN multiple metadata entries WHEN getHistorySearchGroups is called THEN group all entries by their search term`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10)
val result = feature.getHistorySearchGroups(visitsFromSearch + directVisits)
assertEquals(10, result.size)
assertEquals(visitsFromSearch.map { it.key.searchTerm }, result.map { it.groupName })
assertEquals(visitsFromSearch.map { listOf(it) }, result.map { it.groupItems })
}
@Test
fun `GIVEN multiple metadata entries for the same url WHEN getHistorySearchGroups is called THEN entries are deduped`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val newerVisitsFromSearch = visitsFromSearch.map { it.copy(updatedAt = it.updatedAt * 2) }
val directVisits = getDirectVisitsHistoryMetadataItems(10)
val result = feature.getHistorySearchGroups(visitsFromSearch + directVisits + newerVisitsFromSearch)
assertEquals(10, result.size)
assertEquals(newerVisitsFromSearch.map { it.key.searchTerm }, result.map { it.groupName })
assertEquals(
newerVisitsFromSearch.map {
listOf(it.copy(totalViewTime = it.totalViewTime * 2,))
},
result.map { it.groupItems }
)
}
@Test
fun `GIVEN highlights and search groups WHEN getSortedHistory is called THEN sort descending all items based on the last access time`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10)
val expected = directVisits.reversed().toRecentHistoryHighlights()
.zip(visitsFromSearch.toIndividualRecentHistoryGroups())
.flatMap {
listOf(it.first, it.second)
}
val result = feature.getSortedHistory(
directVisits.toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
)
assertEquals(expected, result)
}
@Test
fun `GIVEN highlights don't have a valid title WHEN getSortedHistory is called THEN the url is set as title`() {
val feature = RecentVisitsFeature(mockk(), mockk(), mockk(), mockk(), mockk(), false)
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
val directVisits = getDirectVisitsHistoryMetadataItems(10).mapIndexed { index, item -> val directVisits = getDirectVisitsHistoryMetadataItems(10).mapIndexed { index, item ->
when (index % 3) { when (index % 3) {
0 -> item 0 -> item
@ -606,12 +211,11 @@ class RecentVisitsFeatureTest {
} }
val sortedByDateHighlights = directVisits.reversed() val sortedByDateHighlights = directVisits.reversed()
val result = feature.getSortedHistory( val result = feature.getCombinedHistory(
directVisits.toHistoryHighlightsInternal(), directVisits.toHistoryHighlightsInternal(),
visitsFromSearch.toHistoryGroupsInternal()
).filterIsInstance<RecentHistoryHighlight>() ).filterIsInstance<RecentHistoryHighlight>()
assertEquals(10, result.size) assertEquals(9, result.size)
result.forEachIndexed { index, item -> result.forEachIndexed { index, item ->
when (index % 3) { when (index % 3) {
0 -> assertEquals(sortedByDateHighlights[index].title, item.title) 0 -> assertEquals(sortedByDateHighlights[index].title, item.title)
@ -621,25 +225,6 @@ class RecentVisitsFeatureTest {
} }
} }
@Test
fun `GIVEN highlight visits also exist in search groups WHEN removeHighlightsAlreadyInGroups is called THEN filter out such highlights`() {
val visitsFromSearch = getSearchFromHistoryMetadataItems(10)
// To know if a highlight appears in a search group each visit's url should be checked.
// Ensure we have the identical urls with the ones from a search group and also some random others.
val directDupeVisits = visitsFromSearch.mapIndexed { index, item ->
when (index % 2) {
0 -> item
else -> item.copy(key = item.key.copy(url = "https://mozilla.org"))
}
}
val highlights = directDupeVisits.toHistoryHighlightsInternal()
val result = highlights.removeHighlightsAlreadyInGroups(visitsFromSearch.toHistoryGroupsInternal())
assertEquals(5, result.size)
result.forEach { assertEquals("https://mozilla.org", it.historyHighlight.url) }
}
private fun startRecentVisitsFeature() { private fun startRecentVisitsFeature() {
val feature = RecentVisitsFeature( val feature = RecentVisitsFeature(
appStore, appStore,
@ -647,11 +232,8 @@ class RecentVisitsFeatureTest {
lazy { historyHightlightsStorage }, lazy { historyHightlightsStorage },
scope, scope,
testDispatcher, testDispatcher,
false
) )
assertEquals(emptyList<RecentHistoryGroup>(), appStore.state.recentHistory)
feature.start() feature.start()
scope.advanceUntilIdle() scope.advanceUntilIdle()
@ -746,15 +328,6 @@ private fun HistoryMetadata.toHistoryHighlight(): HistoryHighlight = HistoryHigh
previewImageUrl = null previewImageUrl = null
) )
private fun HistoryMetadata.toRecentHistoryGroup(): RecentHistoryGroup = RecentHistoryGroup(
title = key.searchTerm!!,
historyMetadata = listOf(this)
)
private fun List<HistoryMetadata>.toIndividualRecentHistoryGroups(): List<RecentHistoryGroup> =
map { it.toRecentHistoryGroup() }
.sortedByDescending { it.lastUpdated() }
private fun HistoryMetadata.toRecentHistoryHighlight(): RecentHistoryHighlight = private fun HistoryMetadata.toRecentHistoryHighlight(): RecentHistoryHighlight =
RecentHistoryHighlight( RecentHistoryHighlight(
title = if (title.isNullOrBlank()) key.url else title!!, title = if (title.isNullOrBlank()) key.url else title!!,
@ -784,10 +357,3 @@ private fun HistoryMetadata.toHistoryHighlightInternal(lastAccessTime: Long) =
private fun List<HistoryMetadata>.toHistoryHighlightsInternal() = mapIndexed { index, item -> private fun List<HistoryMetadata>.toHistoryHighlightsInternal() = mapIndexed { index, item ->
item.toHistoryHighlightInternal(index + 1L) item.toHistoryHighlightInternal(index + 1L)
} }
private fun HistoryMetadata.toHistoryGroupInternal() = HistoryGroupInternal(
groupName = key.searchTerm!!,
groupItems = listOf(this)
)
private fun List<HistoryMetadata>.toHistoryGroupsInternal() = map { it.toHistoryGroupInternal() }

@ -5,8 +5,6 @@
package org.mozilla.fenix.home.recentvisits.controller package org.mozilla.fenix.home.recentvisits.controller
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
@ -14,31 +12,22 @@ import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import mozilla.components.browser.state.action.HistoryMetadataAction
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.DocumentType
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.concept.storage.HistoryMetadataStorage import mozilla.components.concept.storage.HistoryMetadataStorage
import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase import mozilla.components.feature.tabs.TabsUseCases.SelectOrAddUseCase
import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.RecentSearches
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -93,69 +82,6 @@ class RecentVisitsControllerTest {
} }
} }
@Test
fun handleRecentHistoryGroupClicked() = runTestOnMain {
val historyEntry = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null),
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
val historyGroup = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(historyEntry)
)
controller.handleRecentHistoryGroupClicked(historyGroup)
verify {
navController.navigate(
match<NavDirections> { it.actionId == R.id.action_global_history_metadata_group }
)
}
}
@Test
fun handleRemoveGroup() = runTestOnMain {
val historyMetadataKey = HistoryMetadataKey(
"http://www.mozilla.com",
"mozilla",
null
)
val historyGroup = RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(
HistoryMetadata(
key = historyMetadataKey,
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
)
)
assertNull(RecentSearches.groupDeleted.testGetValue())
controller.handleRemoveRecentHistoryGroup(historyGroup.title)
advanceUntilIdle()
verify {
store.dispatch(HistoryMetadataAction.DisbandSearchGroupAction(searchTerm = historyGroup.title))
appStore.dispatch(AppAction.DisbandSearchGroupAction(searchTerm = historyGroup.title))
}
assertNotNull(RecentSearches.groupDeleted.testGetValue())
coVerify {
storage.deleteHistoryMetadata(historyGroup.title)
}
}
@Test @Test
fun handleRecentHistoryHighlightClicked() = runTestOnMain { fun handleRecentHistoryHighlightClicked() = runTestOnMain {
val historyHighlight = RecentHistoryHighlight("title", "url") val historyHighlight = RecentHistoryHighlight("title", "url")

@ -6,16 +6,12 @@ package org.mozilla.fenix.home.recentvisits.interactor
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import mozilla.components.concept.storage.DocumentType
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.HistoryMetadataKey
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.home.pocket.PocketStoriesController import org.mozilla.fenix.home.pocket.PocketStoriesController
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
@ -44,67 +40,12 @@ class RecentVisitsInteractorTest {
) )
} }
@Test
fun handleRecentHistoryGroupClicked() {
val historyGroup =
RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(
HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", null, null),
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
)
)
interactor.onRecentHistoryGroupClicked(historyGroup)
verify {
recentVisitsController.handleRecentHistoryGroupClicked(historyGroup)
}
}
@Test @Test
fun handleHistoryShowAllClicked() { fun handleHistoryShowAllClicked() {
interactor.onHistoryShowAllClicked() interactor.onHistoryShowAllClicked()
verify { recentVisitsController.handleHistoryShowAllClicked() } verify { recentVisitsController.handleHistoryShowAllClicked() }
} }
@Test
fun onRemoveRecentHistoryGroup() {
val historyMetadataKey = HistoryMetadataKey(
"http://www.mozilla.com",
"mozilla",
null
)
val historyGroup =
RecentHistoryGroup(
title = "mozilla",
historyMetadata = listOf(
HistoryMetadata(
key = historyMetadataKey,
title = "mozilla",
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
totalViewTime = 10,
documentType = DocumentType.Regular,
previewImageUrl = null
)
)
)
interactor.onRemoveRecentHistoryGroup(historyGroup.title)
verify {
recentVisitsController.handleRemoveRecentHistoryGroup(historyGroup.title)
}
}
@Test @Test
fun onRecentHistoryHighlightClicked() { fun onRecentHistoryHighlightClicked() {
val historyHighlight: RecentHistoryHighlight = mockk() val historyHighlight: RecentHistoryHighlight = mockk()

@ -24,7 +24,7 @@ import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
@ -56,7 +56,7 @@ class SessionControlViewTest {
@Test @Test
fun `GIVEN historyMetadata WHEN calling shouldShowHomeOnboardingDialog THEN show the dialog `() { fun `GIVEN historyMetadata WHEN calling shouldShowHomeOnboardingDialog THEN show the dialog `() {
val historyMetadata = listOf(RecentHistoryGroup("title", emptyList())) val historyMetadata = listOf(RecentlyVisitedItem.RecentHistoryHighlight("title", ""))
val settings: Settings = mockk() val settings: Settings = mockk()
every { settings.hasShownHomeOnboardingDialog } returns false every { settings.hasShownHomeOnboardingDialog } returns false
@ -138,7 +138,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf(RecentBookmark()) val recentBookmarks = listOf(RecentBookmark())
val historyMetadata = emptyList<RecentHistoryGroup>() val historyMetadata = emptyList<RecentlyVisitedItem>()
val pocketStories = emptyList<PocketStory>() val pocketStories = emptyList<PocketStory>()
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true
@ -173,7 +173,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf(RecentBookmark()) val recentBookmarks = listOf(RecentBookmark())
val historyMetadata = emptyList<RecentHistoryGroup>() val historyMetadata = emptyList<RecentlyVisitedItem>()
val pocketStories = emptyList<PocketStory>() val pocketStories = emptyList<PocketStory>()
val nimbusMessageCard: Message = mockk() val nimbusMessageCard: Message = mockk()
@ -206,7 +206,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<RecentBookmark>() val recentBookmarks = listOf<RecentBookmark>()
val historyMetadata = emptyList<RecentHistoryGroup>() val historyMetadata = emptyList<RecentlyVisitedItem>()
val pocketStories = emptyList<PocketStory>() val pocketStories = emptyList<PocketStory>()
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true
@ -241,7 +241,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<RecentBookmark>() val recentBookmarks = listOf<RecentBookmark>()
val historyMetadata = listOf(RecentHistoryGroup("title", emptyList())) val historyMetadata = listOf<RecentlyVisitedItem>(mockk())
val pocketStories = emptyList<PocketStory>() val pocketStories = emptyList<PocketStory>()
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true
@ -276,7 +276,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<RecentBookmark>() val recentBookmarks = listOf<RecentBookmark>()
val historyMetadata = emptyList<RecentHistoryGroup>() val historyMetadata = emptyList<RecentlyVisitedItem>()
val pocketStories = listOf(PocketRecommendedStory("", "", "", "", "", 1, 1)) val pocketStories = listOf(PocketRecommendedStory("", "", "", "", "", 1, 1))
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true
@ -312,7 +312,7 @@ class SessionControlViewTest {
val collections = emptyList<TabCollection>() val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<RecentBookmark>() val recentBookmarks = listOf<RecentBookmark>()
val historyMetadata = emptyList<RecentHistoryGroup>() val historyMetadata = emptyList<RecentlyVisitedItem>()
val pocketStories = emptyList<PocketStory>() val pocketStories = emptyList<PocketStory>()
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true
@ -347,7 +347,7 @@ class SessionControlViewTest {
val collections = listOf(collection) val collections = listOf(collection)
val expandedCollections = emptySet<Long>() val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf<RecentBookmark>(mockk()) val recentBookmarks = listOf<RecentBookmark>(mockk())
val historyMetadata = listOf<RecentHistoryGroup>(mockk()) val historyMetadata = listOf<RecentlyVisitedItem>(mockk())
val pocketStories = listOf<PocketStory>(mockk()) val pocketStories = listOf<PocketStory>(mockk())
every { settings.showTopSitesFeature } returns true every { settings.showTopSitesFeature } returns true

Loading…
Cancel
Save