For #21593 - Refactor out "isSelected" from PocketRecommendedStoriesCategory

Having the list of categories and the list of selected categories separate in
State allows updating them independently.
upstream-sync
Mugurell 3 years ago committed by mergify[bot]
parent 84c61e24a7
commit 565beb88c9

@ -8,7 +8,7 @@ import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketRecommendedStory import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
/** /**
* Get the list of stories to be displayed based on the user selected categories. * Get the list of stories to be displayed based on the user selected categories.
@ -21,9 +21,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommende
fun HomeFragmentState.getFilteredStories( fun HomeFragmentState.getFilteredStories(
neededStoriesCount: Int neededStoriesCount: Int
): List<PocketRecommendedStory> { ): List<PocketRecommendedStory> {
val currentlySelectedCategories = pocketStoriesCategories.filter { it.isSelected } if (pocketStoriesCategoriesSelections.isEmpty()) {
if (currentlySelectedCategories.isEmpty()) {
return pocketStoriesCategories return pocketStoriesCategories
.find { .find {
it.name == POCKET_STORIES_DEFAULT_CATEGORY_NAME it.name == POCKET_STORIES_DEFAULT_CATEGORY_NAME
@ -32,8 +30,13 @@ fun HomeFragmentState.getFilteredStories(
?.take(neededStoriesCount) ?: emptyList() ?.take(neededStoriesCount) ?: emptyList()
} }
val oldestSortedCategories = currentlySelectedCategories val oldestSortedCategories = pocketStoriesCategoriesSelections
.sortedByDescending { it.lastInteractedWithTimestamp } .sortedByDescending { it.selectionTimestamp }
.map { selectedCategory ->
pocketStoriesCategories.first {
it.name == selectedCategory.name
}
}
val filteredStoriesCount = getFilteredStoriesCount( val filteredStoriesCount = getFilteredStoriesCount(
oldestSortedCategories, neededStoriesCount oldestSortedCategories, neededStoriesCount
@ -57,7 +60,7 @@ fun HomeFragmentState.getFilteredStories(
@VisibleForTesting @VisibleForTesting
@Suppress("ReturnCount", "NestedBlockDepth") @Suppress("ReturnCount", "NestedBlockDepth")
internal fun getFilteredStoriesCount( internal fun getFilteredStoriesCount(
selectedCategories: List<PocketRecommendedStoryCategory>, selectedCategories: List<PocketRecommendedStoriesCategory>,
neededStoriesCount: Int neededStoriesCount: Int
): Map<String, Int> { ): Map<String, Int> {
val totalStoriesInFilteredCategories = selectedCategories.fold(0) { availableStories, category -> val totalStoriesInFilteredCategories = selectedCategories.fold(0) { availableStories, category ->

@ -116,7 +116,7 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.DefaultPocketStoriesController import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
@ -258,7 +258,7 @@ class HomeFragment : Fragment() {
if (requireContext().settings().showPocketRecommendationsFeature) { if (requireContext().settings().showPocketRecommendationsFeature) {
val categories = components.core.pocketStoriesService.getStories() val categories = components.core.pocketStoriesService.getStories()
.groupBy { story -> story.category } .groupBy { story -> story.category }
.map { (category, stories) -> PocketRecommendedStoryCategory(category, stories) } .map { (category, stories) -> PocketRecommendedStoriesCategory(category, stories) }
homeFragmentStore.dispatch(HomeFragmentAction.PocketStoriesCategoriesChange(categories)) homeFragmentStore.dispatch(HomeFragmentAction.PocketStoriesCategoriesChange(categories))
} else { } else {

@ -18,7 +18,8 @@ import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.recenttabs.RecentTab 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.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
/** /**
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s. * The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.
@ -69,7 +70,8 @@ data class HomeFragmentState(
val recentBookmarks: List<BookmarkNode> = emptyList(), val recentBookmarks: List<BookmarkNode> = emptyList(),
val historyMetadata: List<HistoryMetadataGroup> = emptyList(), val historyMetadata: List<HistoryMetadataGroup> = emptyList(),
val pocketStories: List<PocketRecommendedStory> = emptyList(), val pocketStories: List<PocketRecommendedStory> = emptyList(),
val pocketStoriesCategories: List<PocketRecommendedStoryCategory> = emptyList() val pocketStoriesCategories: List<PocketRecommendedStoriesCategory> = emptyList(),
val pocketStoriesCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList()
) : State ) : State
sealed class HomeFragmentAction : Action { sealed class HomeFragmentAction : Action {
@ -99,7 +101,7 @@ sealed class HomeFragmentAction : Action {
data class DeselectPocketStoriesCategory(val categoryName: String) : HomeFragmentAction() data class DeselectPocketStoriesCategory(val categoryName: String) : HomeFragmentAction()
data class PocketStoriesShown(val storiesShown: List<PocketRecommendedStory>) : HomeFragmentAction() data class PocketStoriesShown(val storiesShown: List<PocketRecommendedStory>) : HomeFragmentAction()
data class PocketStoriesChange(val pocketStories: List<PocketRecommendedStory>) : HomeFragmentAction() data class PocketStoriesChange(val pocketStories: List<PocketRecommendedStory>) : HomeFragmentAction()
data class PocketStoriesCategoriesChange(val storiesCategories: List<PocketRecommendedStoryCategory>) : data class PocketStoriesCategoriesChange(val storiesCategories: List<PocketRecommendedStoriesCategory>) :
HomeFragmentAction() HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction() object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction() object RemoveSetDefaultBrowserCard : HomeFragmentAction()
@ -145,29 +147,26 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks) is HomeFragmentAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks)
is HomeFragmentAction.HistoryMetadataChange -> state.copy(historyMetadata = action.historyMetadata) is HomeFragmentAction.HistoryMetadataChange -> state.copy(historyMetadata = action.historyMetadata)
is HomeFragmentAction.SelectPocketStoriesCategory -> { is HomeFragmentAction.SelectPocketStoriesCategory -> {
// Selecting a category means the stories to be displayed needs to also be changed.
val updatedCategoriesState = state.copy( val updatedCategoriesState = state.copy(
pocketStoriesCategories = state.pocketStoriesCategories.map { pocketStoriesCategoriesSelections =
when (it.name == action.categoryName) { state.pocketStoriesCategoriesSelections + PocketRecommendedStoriesSelectedCategory(
true -> it.copy(isSelected = true, lastInteractedWithTimestamp = System.currentTimeMillis()) name = action.categoryName
false -> it )
}
}
) )
// Selecting a category means the stories to be displayed needs to also be changed.
return updatedCategoriesState.copy( return updatedCategoriesState.copy(
pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT)
) )
} }
is HomeFragmentAction.DeselectPocketStoriesCategory -> { is HomeFragmentAction.DeselectPocketStoriesCategory -> {
val updatedCategoriesState = state.copy( val updatedCategoriesState = state.copy(
// Deselecting a category means the stories to be displayed needs to also be changed. pocketStoriesCategoriesSelections = state.pocketStoriesCategoriesSelections.filterNot {
pocketStoriesCategories = state.pocketStoriesCategories.map { it.name == action.categoryName
when (it.name == action.categoryName) {
true -> it.copy(isSelected = false, lastInteractedWithTimestamp = System.currentTimeMillis())
false -> it
}
} }
) )
// Deselecting a category means the stories to be displayed needs to also be changed.
return updatedCategoriesState.copy( return updatedCategoriesState.copy(
pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT)
) )

@ -18,7 +18,7 @@ import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksControll
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
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.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesController import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesController
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesInteractor import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesInteractor
@ -391,7 +391,7 @@ class SessionControlInteractor(
controller.handleCustomizeHomeTapped() controller.handleCustomizeHomeTapped()
} }
override fun onCategoryClick(categoryClicked: PocketRecommendedStoryCategory) { override fun onCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) {
pocketStoriesController.handleCategoryClick(categoryClicked) pocketStoriesController.handleCategoryClick(categoryClicked)
} }

@ -13,18 +13,15 @@ import mozilla.components.service.pocket.PocketRecommendedStory
const val POCKET_STORIES_DEFAULT_CATEGORY_NAME = "general" const val POCKET_STORIES_DEFAULT_CATEGORY_NAME = "general"
/** /**
* Pocket assigned topic of interest for each story. * In memory cache of Pocket assigned topic of interest for recommended stories.
* Avoids multiple stories mappings for each time we are interested in their categories.
* *
* One to many relationship with [PocketRecommendedStory]es. * One to many relationship with [PocketRecommendedStory]es.
* *
* @property name The exact name of each category. Case sensitive. * @property name The exact name of each category. Case sensitive.
* @property stories All [PocketRecommendedStory]es with this category. * @property stories All [PocketRecommendedStory]s with this category.
* @property isSelected Whether this category is currently selected by the user.
* @property lastInteractedWithTimestamp Last time the user selected or deselected this category.
*/ */
data class PocketRecommendedStoryCategory( data class PocketRecommendedStoriesCategory(
val name: String, val name: String,
val stories: List<PocketRecommendedStory> = emptyList(), val stories: List<PocketRecommendedStory> = emptyList()
val isSelected: Boolean = false,
val lastInteractedWithTimestamp: Long = 0L
) )

@ -0,0 +1,16 @@
/* 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.home.sessioncontrol.viewholders.pocket
/**
* Details about a selected Pocket recommended stories category.
*
* @property name The exact name of a selected category. Case sensitive.
* @property selectionTimestamp The exact time at which a category was selected. Defaults to [System.currentTimeMillis].
*/
data class PocketRecommendedStoriesSelectedCategory(
val name: String,
val selectionTimestamp: Long = System.currentTimeMillis()
)

@ -150,21 +150,22 @@ fun PocketStories(
} }
/** /**
* Displays a list of [PocketRecommendedStoryCategory]. * Displays a list of [PocketRecommendedStoriesCategory]s.
* *
* @param categories The categories needed to be displayed. * @param categories The categories needed to be displayed.
* @param onCategoryClick Callback for when the user taps a category. * @param onCategoryClick Callback for when the user taps a category.
*/ */
@Composable @Composable
fun PocketStoriesCategories( fun PocketStoriesCategories(
categories: List<PocketRecommendedStoryCategory>, categories: List<PocketRecommendedStoriesCategory>,
onCategoryClick: (PocketRecommendedStoryCategory) -> Unit selections: List<PocketRecommendedStoriesSelectedCategory>,
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit
) { ) {
StaggeredHorizontalGrid( StaggeredHorizontalGrid(
horizontalItemsSpacing = 16.dp horizontalItemsSpacing = 16.dp
) { ) {
categories.filter { it.name != POCKET_STORIES_DEFAULT_CATEGORY_NAME }.forEach { category -> categories.filter { it.name != POCKET_STORIES_DEFAULT_CATEGORY_NAME }.forEach { category ->
SelectableChip(category.name, category.isSelected) { SelectableChip(category.name, selections.map { it.name }.contains(category.name)) {
onCategoryClick(category) onCategoryClick(category)
} }
} }
@ -241,8 +242,9 @@ private fun PocketStoriesComposablesPreview() {
PocketStoriesCategories( PocketStoriesCategories(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor".split(" ").map { "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor".split(" ").map {
PocketRecommendedStoryCategory(it) PocketRecommendedStoriesCategory(it)
} },
emptyList()
) { } ) { }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))

@ -19,11 +19,11 @@ import org.mozilla.fenix.R
*/ */
interface PocketStoriesController { interface PocketStoriesController {
/** /**
* Callback allowing to handle a specific [PocketRecommendedStoryCategory] being clicked by the user. * Callback allowing to handle a specific [PocketRecommendedStoriesCategory] being clicked by the user.
* *
* @param categoryClicked the just clicked [PocketRecommendedStoryCategory]. * @param categoryClicked the just clicked [PocketRecommendedStoriesCategory].
*/ */
fun handleCategoryClick(categoryClicked: PocketRecommendedStoryCategory): Unit fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory): Unit
/** /**
* Callback to decide what should happen as an effect of a new list of stories being shown. * Callback to decide what should happen as an effect of a new list of stories being shown.
@ -52,30 +52,20 @@ internal class DefaultPocketStoriesController(
private val homeStore: HomeFragmentStore, private val homeStore: HomeFragmentStore,
private val navController: NavController private val navController: NavController
) : PocketStoriesController { ) : PocketStoriesController {
override fun handleCategoryClick(categoryClicked: PocketRecommendedStoryCategory) { override fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) {
val allCategories = homeStore.state.pocketStoriesCategories val initialCategoriesSelections = homeStore.state.pocketStoriesCategoriesSelections
// First check whether the category is clicked to be deselected. // First check whether the category is clicked to be deselected.
if (categoryClicked.isSelected) { if (initialCategoriesSelections.map { it.name }.contains(categoryClicked.name)) {
homeStore.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(categoryClicked.name)) homeStore.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(categoryClicked.name))
return return
} }
// If a new category is clicked to be selected: // If a new category is clicked to be selected:
// Ensure the number of categories selected at a time is capped. // Ensure the number of categories selected at a time is capped.
val currentlySelectedCategoriesCount = allCategories.fold(0) { count, category ->
if (category.isSelected) count + 1 else count
}
val oldestCategoryToDeselect = val oldestCategoryToDeselect =
if (currentlySelectedCategoriesCount == POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT) { if (initialCategoriesSelections.size == POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT) {
allCategories initialCategoriesSelections.minByOrNull { it.selectionTimestamp }
.filter { it.isSelected }
.reduce { oldestSelected, category ->
when (oldestSelected.lastInteractedWithTimestamp <= category.lastInteractedWithTimestamp) {
true -> oldestSelected
false -> category
}
}
} else { } else {
null null
} }

@ -13,9 +13,9 @@ interface PocketStoriesInteractor {
/** /**
* Callback for when the user clicked a specific category. * Callback for when the user clicked a specific category.
* *
* @param categoryClicked the just clicked [PocketRecommendedStoryCategory]. * @param categoryClicked the just clicked [PocketRecommendedStoriesCategory].
*/ */
fun onCategoryClick(categoryClicked: PocketRecommendedStoryCategory) fun onCategoryClick(categoryClicked: PocketRecommendedStoriesCategory)
/** /**
* Callback for then new stories are shown to the user. * Callback for then new stories are shown to the user.

@ -72,7 +72,7 @@ fun PocketStories(
store: HomeFragmentStore, store: HomeFragmentStore,
client: Client, client: Client,
onStoriesShown: (List<PocketRecommendedStory>) -> Unit, onStoriesShown: (List<PocketRecommendedStory>) -> Unit,
onCategoryClick: (PocketRecommendedStoryCategory) -> Unit, onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
onExternalLinkClicked: (String) -> Unit onExternalLinkClicked: (String) -> Unit
) { ) {
val stories = store val stories = store
@ -81,6 +81,9 @@ fun PocketStories(
val categories = store val categories = store
.observeAsComposableState { state -> state.pocketStoriesCategories }.value .observeAsComposableState { state -> state.pocketStoriesCategories }.value
val categoriesSelections = store
.observeAsComposableState { state -> state.pocketStoriesCategoriesSelections }.value
LaunchedEffect(stories) { LaunchedEffect(stories) {
// We should report back when a certain story is actually being displayed. // We should report back when a certain story is actually being displayed.
// Cannot do it reliably so for now we'll just mass report everything as being displayed. // Cannot do it reliably so for now we'll just mass report everything as being displayed.
@ -109,7 +112,10 @@ fun PocketStories(
Spacer(Modifier.height(17.dp)) Spacer(Modifier.height(17.dp))
PocketStoriesCategories(categories ?: emptyList()) { PocketStoriesCategories(
categories = categories ?: emptyList(),
selections = categoriesSelections ?: emptyList()
) {
onCategoryClick(it) onCategoryClick(it)
} }

@ -12,15 +12,16 @@ import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
import kotlin.random.Random import kotlin.random.Random
class HomeFragmentStateTest { class HomeFragmentStateTest {
private val otherStoriesCategory = private val otherStoriesCategory =
PocketRecommendedStoryCategory("other", getFakePocketStories(3, "other")) PocketRecommendedStoriesCategory("other", getFakePocketStories(3, "other"))
private val anotherStoriesCategory = private val anotherStoriesCategory =
PocketRecommendedStoryCategory("another", getFakePocketStories(3, "another")) PocketRecommendedStoriesCategory("another", getFakePocketStories(3, "another"))
private val defaultStoriesCategory = PocketRecommendedStoryCategory( private val defaultStoriesCategory = PocketRecommendedStoriesCategory(
POCKET_STORIES_DEFAULT_CATEGORY_NAME, POCKET_STORIES_DEFAULT_CATEGORY_NAME,
getFakePocketStories(3) getFakePocketStories(3)
) )
@ -60,9 +61,8 @@ class HomeFragmentStateTest {
@Test @Test
fun `GIVEN a category is selected WHEN getFilteredStories is called for fewer than in the category THEN only stories from that category are returned`() { fun `GIVEN a category is selected WHEN getFilteredStories is called for fewer than in the category THEN only stories from that category are returned`() {
val homeState = HomeFragmentState( val homeState = HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory),
otherStoriesCategory.copy(isSelected = true), anotherStoriesCategory, defaultStoriesCategory pocketStoriesCategoriesSelections = listOf(PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name))
)
) )
var result = homeState.getFilteredStories(2) var result = homeState.getFilteredStories(2)
@ -77,10 +77,10 @@ class HomeFragmentStateTest {
@Test @Test
fun `GIVEN two categories are selected WHEN getFilteredStories is called for fewer than in both THEN only stories from those categories are returned`() { fun `GIVEN two categories are selected WHEN getFilteredStories is called for fewer than in both THEN only stories from those categories are returned`() {
val homeState = HomeFragmentState( val homeState = HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory),
otherStoriesCategory.copy(isSelected = true), pocketStoriesCategoriesSelections = listOf(
anotherStoriesCategory.copy(isSelected = true), PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name),
defaultStoriesCategory PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name)
) )
) )
@ -103,19 +103,19 @@ class HomeFragmentStateTest {
@Test @Test
fun `GIVEN two categories are selected WHEN getFilteredStories is called for an odd number of stories THEN there are more by one stories from the newest category`() { fun `GIVEN two categories are selected WHEN getFilteredStories is called for an odd number of stories THEN there are more by one stories from the newest category`() {
val firstSelectedCategory = otherStoriesCategory.copy(lastInteractedWithTimestamp = 0, isSelected = true)
val lastSelectedCategory = anotherStoriesCategory.copy(lastInteractedWithTimestamp = 1, isSelected = true)
val homeState = HomeFragmentState( val homeState = HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory),
firstSelectedCategory, lastSelectedCategory, defaultStoriesCategory pocketStoriesCategoriesSelections = listOf(
PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name, selectionTimestamp = 0),
PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name, selectionTimestamp = 1)
) )
) )
val result = homeState.getFilteredStories(5) val result = homeState.getFilteredStories(5)
assertEquals(5, result.size) assertEquals(5, result.size)
assertEquals(2, result.filter { it.category == firstSelectedCategory.name }.size) assertEquals(2, result.filter { it.category == otherStoriesCategory.name }.size)
assertEquals(3, result.filter { it.category == lastSelectedCategory.name }.size) assertEquals(3, result.filter { it.category == anotherStoriesCategory.name }.size)
} }
@Test @Test
@ -209,8 +209,8 @@ class HomeFragmentStateTest {
@Test @Test
fun `GIVEN two categories selected with more than needed stories WHEN getFilteredStories is called THEN the results are sorted in the order of least shown`() { fun `GIVEN two categories selected with more than needed stories WHEN getFilteredStories is called THEN the results are sorted in the order of least shown`() {
val firstCategory = PocketRecommendedStoryCategory( val firstCategory = PocketRecommendedStoriesCategory(
"first", getFakePocketStories(3, "first"), true, 0 "first", getFakePocketStories(3, "first")
).run { ).run {
// Avoid the first item also being the oldest to eliminate a potential bug in code // Avoid the first item also being the oldest to eliminate a potential bug in code
// that would still get the expected result. // that would still get the expected result.
@ -224,8 +224,8 @@ class HomeFragmentStateTest {
} }
) )
} }
val secondCategory = PocketRecommendedStoryCategory( val secondCategory = PocketRecommendedStoriesCategory(
"second", getFakePocketStories(3, "second"), true, 222 "second", getFakePocketStories(3, "second")
).run { ).run {
// Avoid the first item also being the oldest to eliminate a potential bug in code // Avoid the first item also being the oldest to eliminate a potential bug in code
// that would still get the expected result. // that would still get the expected result.
@ -240,7 +240,13 @@ class HomeFragmentStateTest {
) )
} }
val homeState = HomeFragmentState(pocketStoriesCategories = listOf(firstCategory, secondCategory)) val homeState = HomeFragmentState(
pocketStoriesCategories = listOf(firstCategory, secondCategory),
pocketStoriesCategoriesSelections = listOf(
PocketRecommendedStoriesSelectedCategory(firstCategory.name, selectionTimestamp = 0),
PocketRecommendedStoriesSelectedCategory(secondCategory.name, selectionTimestamp = 222)
)
)
val result = homeState.getFilteredStories(6) val result = homeState.getFilteredStories(6)

@ -28,7 +28,8 @@ import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.recenttabs.RecentTab 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.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
class HomeFragmentStoreTest { class HomeFragmentStoreTest {
@ -189,8 +190,8 @@ class HomeFragmentStoreTest {
@Test @Test
fun `Test selecting a Pocket recommendations category`() = runBlocking { fun `Test selecting a Pocket recommendations category`() = runBlocking {
val otherStoriesCategory = PocketRecommendedStoryCategory("other") val otherStoriesCategory = PocketRecommendedStoriesCategory("other")
val anotherStoriesCategory = PocketRecommendedStoryCategory("another") val anotherStoriesCategory = PocketRecommendedStoriesCategory("another")
val filteredStories = listOf(mockk<PocketRecommendedStory>()) val filteredStories = listOf(mockk<PocketRecommendedStory>())
homeFragmentStore = HomeFragmentStore( homeFragmentStore = HomeFragmentStore(
HomeFragmentState( HomeFragmentState(
@ -208,7 +209,7 @@ class HomeFragmentStoreTest {
verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) }
} }
val selectedCategories = homeFragmentStore.state.pocketStoriesCategories.filter { it.isSelected } val selectedCategories = homeFragmentStore.state.pocketStoriesCategoriesSelections
assertEquals(1, selectedCategories.size) assertEquals(1, selectedCategories.size)
assertTrue(otherStoriesCategory.name === selectedCategories[0].name) assertTrue(otherStoriesCategory.name === selectedCategories[0].name)
assertSame(filteredStories, homeFragmentStore.state.pocketStories) assertSame(filteredStories, homeFragmentStore.state.pocketStories)
@ -216,13 +217,15 @@ class HomeFragmentStoreTest {
@Test @Test
fun `Test deselecting a Pocket recommendations category`() = runBlocking { fun `Test deselecting a Pocket recommendations category`() = runBlocking {
val otherStoriesCategory = PocketRecommendedStoryCategory("other", isSelected = true) val otherStoriesCategory = PocketRecommendedStoriesCategory("other")
val anotherStoriesCategory = PocketRecommendedStoryCategory("another", isSelected = true) val anotherStoriesCategory = PocketRecommendedStoriesCategory("another")
val filteredStories = listOf(mockk<PocketRecommendedStory>()) val filteredStories = listOf(mockk<PocketRecommendedStory>())
homeFragmentStore = HomeFragmentStore( homeFragmentStore = HomeFragmentStore(
HomeFragmentState( HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory),
otherStoriesCategory, anotherStoriesCategory pocketStoriesCategoriesSelections = listOf(
PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name),
PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name)
) )
) )
) )
@ -235,10 +238,9 @@ class HomeFragmentStoreTest {
verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) }
} }
assertTrue( val selectedCategories = homeFragmentStore.state.pocketStoriesCategoriesSelections
listOf(anotherStoriesCategory) assertEquals(1, selectedCategories.size)
.containsAll(homeFragmentStore.state.pocketStoriesCategories.filter { it.isSelected }) assertTrue(anotherStoriesCategory.name === selectedCategories[0].name)
)
assertSame(filteredStories, homeFragmentStore.state.pocketStories) assertSame(filteredStories, homeFragmentStore.state.pocketStories)
} }
@ -259,8 +261,8 @@ class HomeFragmentStoreTest {
@Test @Test
fun `Test updating the list of Pocket recommendations categories`() = runBlocking { fun `Test updating the list of Pocket recommendations categories`() = runBlocking {
val otherStoriesCategory = PocketRecommendedStoryCategory("other") val otherStoriesCategory = PocketRecommendedStoriesCategory("other")
val anotherStoriesCategory = PocketRecommendedStoryCategory("another", isSelected = true) val anotherStoriesCategory = PocketRecommendedStoriesCategory("another")
homeFragmentStore = HomeFragmentStore(HomeFragmentState()) homeFragmentStore = HomeFragmentStore(HomeFragmentState())
mockkStatic("org.mozilla.fenix.ext.HomeFragmentStateKt") { mockkStatic("org.mozilla.fenix.ext.HomeFragmentStateKt") {
@ -268,9 +270,7 @@ class HomeFragmentStoreTest {
every { any<HomeFragmentState>().getFilteredStories(any()) } returns firstFilteredStories every { any<HomeFragmentState>().getFilteredStories(any()) } returns firstFilteredStories
homeFragmentStore.dispatch( homeFragmentStore.dispatch(
HomeFragmentAction.PocketStoriesCategoriesChange( HomeFragmentAction.PocketStoriesCategoriesChange(listOf(otherStoriesCategory, anotherStoriesCategory))
listOf(otherStoriesCategory, anotherStoriesCategory)
)
).join() ).join()
verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) }
assertTrue( assertTrue(
@ -280,7 +280,7 @@ class HomeFragmentStoreTest {
) )
assertSame(firstFilteredStories, homeFragmentStore.state.pocketStories) assertSame(firstFilteredStories, homeFragmentStore.state.pocketStories)
val updatedCategories = listOf(PocketRecommendedStoryCategory("yetAnother")) val updatedCategories = listOf(PocketRecommendedStoriesCategory("yetAnother"))
val secondFilteredStories = listOf(mockk<PocketRecommendedStory>()) val secondFilteredStories = listOf(mockk<PocketRecommendedStory>())
every { any<HomeFragmentState>().getFilteredStories(any()) } returns secondFilteredStories every { any<HomeFragmentState>().getFilteredStories(any()) } returns secondFilteredStories
homeFragmentStore.dispatch( homeFragmentStore.dispatch(

@ -18,7 +18,7 @@ import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksControll
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesController import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesController
class SessionControlInteractorTest { class SessionControlInteractorTest {
@ -215,7 +215,7 @@ class SessionControlInteractorTest {
@Test @Test
fun `GIVEN a PocketStoriesInteractor WHEN a category is clicked THEN handle it in a PocketStoriesController`() { fun `GIVEN a PocketStoriesInteractor WHEN a category is clicked THEN handle it in a PocketStoriesController`() {
val clickedCategory: PocketRecommendedStoryCategory = mockk() val clickedCategory: PocketRecommendedStoriesCategory = mockk()
interactor.onCategoryClick(clickedCategory) interactor.onCategoryClick(clickedCategory)

@ -21,11 +21,15 @@ import org.mozilla.fenix.home.HomeFragmentStore
class DefaultPocketStoriesControllerTest { class DefaultPocketStoriesControllerTest {
@Test @Test
fun `GIVEN a category is selected WHEN that same category is clicked THEN deselect it`() { fun `GIVEN a category is selected WHEN that same category is clicked THEN deselect it`() {
val category1 = PocketRecommendedStoryCategory("cat1", emptyList(), isSelected = false) val category1 = PocketRecommendedStoriesCategory("cat1", emptyList())
val category2 = PocketRecommendedStoryCategory("cat2", emptyList(), isSelected = true) val category2 = PocketRecommendedStoriesCategory("cat2", emptyList())
val selections = listOf(PocketRecommendedStoriesSelectedCategory(category2.name))
val store = spyk( val store = spyk(
HomeFragmentStore( HomeFragmentStore(
HomeFragmentState(pocketStoriesCategories = listOf(category1, category2)) HomeFragmentState(
pocketStoriesCategories = listOf(category1, category2),
pocketStoriesCategoriesSelections = selections
)
) )
) )
val controller = DefaultPocketStoriesController(mockk(), store, mockk()) val controller = DefaultPocketStoriesController(mockk(), store, mockk())
@ -39,23 +43,19 @@ class DefaultPocketStoriesControllerTest {
@Test @Test
fun `GIVEN 8 categories are selected WHEN when a new one is clicked THEN the oldest selected is deselected before selecting the new one`() { fun `GIVEN 8 categories are selected WHEN when a new one is clicked THEN the oldest selected is deselected before selecting the new one`() {
val category1 = PocketRecommendedStoryCategory( val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111)
"cat1", emptyList(), isSelected = true, lastInteractedWithTimestamp = 111 val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222)
) val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333)
val category2 = category1.copy("cat2", lastInteractedWithTimestamp = 222) val oldestSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "oldestSelectedCategory", selectionTimestamp = 0)
val category3 = category1.copy("cat3", lastInteractedWithTimestamp = 333) val category4 = PocketRecommendedStoriesSelectedCategory(name = "cat4", selectionTimestamp = 444)
val oldestSelectedCategory = category1.copy("oldestSelectedCategory", lastInteractedWithTimestamp = 0) val category5 = PocketRecommendedStoriesSelectedCategory(name = "cat5", selectionTimestamp = 555)
val category4 = category1.copy("cat4", lastInteractedWithTimestamp = 444) val category6 = PocketRecommendedStoriesSelectedCategory(name = "cat6", selectionTimestamp = 678)
val category5 = category1.copy("cat5", lastInteractedWithTimestamp = 555) val category7 = PocketRecommendedStoriesSelectedCategory(name = "cat7", selectionTimestamp = 890)
val category6 = category1.copy("cat6", lastInteractedWithTimestamp = 678) val newSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "newSelectedCategory", selectionTimestamp = 654321)
val category7 = category1.copy("cat6", lastInteractedWithTimestamp = 890)
val newSelectedCategory = category1.copy(
"newSelectedCategory", isSelected = false, lastInteractedWithTimestamp = 654321
)
val store = spyk( val store = spyk(
HomeFragmentStore( HomeFragmentStore(
HomeFragmentState( HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategoriesSelections = listOf(
category1, category2, category3, category4, category5, category6, category7, oldestSelectedCategory category1, category2, category3, category4, category5, category6, category7, oldestSelectedCategory
) )
) )
@ -63,7 +63,7 @@ class DefaultPocketStoriesControllerTest {
) )
val controller = DefaultPocketStoriesController(mockk(), store, mockk()) val controller = DefaultPocketStoriesController(mockk(), store, mockk())
controller.handleCategoryClick(newSelectedCategory) controller.handleCategoryClick(PocketRecommendedStoriesCategory(newSelectedCategory.name))
verify { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) } verify { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) }
verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategory.name)) } verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategory.name)) }
@ -71,22 +71,17 @@ class DefaultPocketStoriesControllerTest {
@Test @Test
fun `GIVEN fewer than 8 categories are selected WHEN when a new one is clicked THEN don't deselect anything but select the newly clicked category`() { fun `GIVEN fewer than 8 categories are selected WHEN when a new one is clicked THEN don't deselect anything but select the newly clicked category`() {
val category1 = PocketRecommendedStoryCategory( val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111)
"cat1", emptyList(), isSelected = true, lastInteractedWithTimestamp = 111 val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222)
) val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333)
val category2 = category1.copy("cat2", lastInteractedWithTimestamp = 222) val oldestSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "oldestSelectedCategory", selectionTimestamp = 0)
val category3 = category1.copy("cat3", lastInteractedWithTimestamp = 333) val category4 = PocketRecommendedStoriesSelectedCategory(name = "cat4", selectionTimestamp = 444)
val oldestSelectedCategory = category1.copy("oldestSelectedCategory", lastInteractedWithTimestamp = 0) val category5 = PocketRecommendedStoriesSelectedCategory(name = "cat5", selectionTimestamp = 555)
val category4 = category1.copy("cat4", lastInteractedWithTimestamp = 444) val category6 = PocketRecommendedStoriesSelectedCategory(name = "cat6", selectionTimestamp = 678)
val category5 = category1.copy("cat5", lastInteractedWithTimestamp = 555)
val category6 = category1.copy("cat6", lastInteractedWithTimestamp = 678)
val newSelectedCategory = category1.copy(
"newSelectedCategory", isSelected = false, lastInteractedWithTimestamp = 654321
)
val store = spyk( val store = spyk(
HomeFragmentStore( HomeFragmentStore(
HomeFragmentState( HomeFragmentState(
pocketStoriesCategories = listOf( pocketStoriesCategoriesSelections = listOf(
category1, category2, category3, category4, category5, category6, oldestSelectedCategory category1, category2, category3, category4, category5, category6, oldestSelectedCategory
) )
) )
@ -94,10 +89,10 @@ class DefaultPocketStoriesControllerTest {
) )
val controller = DefaultPocketStoriesController(mockk(), store, mockk()) val controller = DefaultPocketStoriesController(mockk(), store, mockk())
controller.handleCategoryClick(newSelectedCategory) controller.handleCategoryClick(PocketRecommendedStoriesCategory("newSelectedCategory"))
verify(exactly = 0) { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) } verify(exactly = 0) { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) }
verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategory.name)) } verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory("newSelectedCategory")) }
} }
@Test @Test

Loading…
Cancel
Save