From 565beb88c98b21bb87a3cd5cb263f54441f9f601 Mon Sep 17 00:00:00 2001 From: Mugurell Date: Mon, 4 Oct 2021 09:22:25 +0300 Subject: [PATCH] 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. --- .../mozilla/fenix/ext/HomeFragmentState.kt | 17 +++--- .../org/mozilla/fenix/home/HomeFragment.kt | 4 +- .../mozilla/fenix/home/HomeFragmentStore.kt | 31 +++++----- .../SessionControlInteractor.kt | 4 +- ...kt => PocketRecommendedStoriesCategory.kt} | 13 ++-- ...ocketRecommendedStoriesSelectedCategory.kt | 16 +++++ .../pocket/PocketStoriesComposables.kt | 14 +++-- .../pocket/PocketStoriesController.kt | 26 +++----- .../pocket/PocketStoriesInteractor.kt | 4 +- .../pocket/PocketStoriesViewHolder.kt | 10 ++- .../fenix/ext/HomeFragmentStateTest.kt | 50 ++++++++------- .../fenix/home/HomeFragmentStoreTest.kt | 36 +++++------ .../home/SessionControlInteractorTest.kt | 4 +- .../DefaultPocketStoriesControllerTest.kt | 61 +++++++++---------- 14 files changed, 152 insertions(+), 138 deletions(-) rename app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/{PocketRecommendedStoryCategory.kt => PocketRecommendedStoriesCategory.kt} (58%) create mode 100644 app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesSelectedCategory.kt diff --git a/app/src/main/java/org/mozilla/fenix/ext/HomeFragmentState.kt b/app/src/main/java/org/mozilla/fenix/ext/HomeFragmentState.kt index 5f3734f832..488b3e0095 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/HomeFragmentState.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/HomeFragmentState.kt @@ -8,7 +8,7 @@ import androidx.annotation.VisibleForTesting import mozilla.components.service.pocket.PocketRecommendedStory 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.PocketRecommendedStoryCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory /** * 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( neededStoriesCount: Int ): List { - val currentlySelectedCategories = pocketStoriesCategories.filter { it.isSelected } - - if (currentlySelectedCategories.isEmpty()) { + if (pocketStoriesCategoriesSelections.isEmpty()) { return pocketStoriesCategories .find { it.name == POCKET_STORIES_DEFAULT_CATEGORY_NAME @@ -32,8 +30,13 @@ fun HomeFragmentState.getFilteredStories( ?.take(neededStoriesCount) ?: emptyList() } - val oldestSortedCategories = currentlySelectedCategories - .sortedByDescending { it.lastInteractedWithTimestamp } + val oldestSortedCategories = pocketStoriesCategoriesSelections + .sortedByDescending { it.selectionTimestamp } + .map { selectedCategory -> + pocketStoriesCategories.first { + it.name == selectedCategory.name + } + } val filteredStoriesCount = getFilteredStoriesCount( oldestSortedCategories, neededStoriesCount @@ -57,7 +60,7 @@ fun HomeFragmentState.getFilteredStories( @VisibleForTesting @Suppress("ReturnCount", "NestedBlockDepth") internal fun getFilteredStoriesCount( - selectedCategories: List, + selectedCategories: List, neededStoriesCount: Int ): Map { val totalStoriesInFilteredCategories = selectedCategories.fold(0) { availableStories, category -> diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 32247d0aad..6f89c3ea22 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -116,7 +116,7 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlView 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.PocketRecommendedStoryCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.settings.SupportUtils @@ -258,7 +258,7 @@ class HomeFragment : Fragment() { if (requireContext().settings().showPocketRecommendationsFeature) { val categories = components.core.pocketStoriesService.getStories() .groupBy { story -> story.category } - .map { (category, stories) -> PocketRecommendedStoryCategory(category, stories) } + .map { (category, stories) -> PocketRecommendedStoriesCategory(category, stories) } homeFragmentStore.dispatch(HomeFragmentAction.PocketStoriesCategoriesChange(categories)) } else { diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt index 6bf220da1e..9415324565 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -18,7 +18,8 @@ import org.mozilla.fenix.ext.getFilteredStories import org.mozilla.fenix.historymetadata.HistoryMetadataGroup import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_TO_SHOW_COUNT -import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory /** * The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s. @@ -69,7 +70,8 @@ data class HomeFragmentState( val recentBookmarks: List = emptyList(), val historyMetadata: List = emptyList(), val pocketStories: List = emptyList(), - val pocketStoriesCategories: List = emptyList() + val pocketStoriesCategories: List = emptyList(), + val pocketStoriesCategoriesSelections: List = emptyList() ) : State sealed class HomeFragmentAction : Action { @@ -99,7 +101,7 @@ sealed class HomeFragmentAction : Action { data class DeselectPocketStoriesCategory(val categoryName: String) : HomeFragmentAction() data class PocketStoriesShown(val storiesShown: List) : HomeFragmentAction() data class PocketStoriesChange(val pocketStories: List) : HomeFragmentAction() - data class PocketStoriesCategoriesChange(val storiesCategories: List) : + data class PocketStoriesCategoriesChange(val storiesCategories: List) : HomeFragmentAction() object RemoveCollectionsPlaceholder : HomeFragmentAction() object RemoveSetDefaultBrowserCard : HomeFragmentAction() @@ -145,29 +147,26 @@ private fun homeFragmentStateReducer( is HomeFragmentAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks) is HomeFragmentAction.HistoryMetadataChange -> state.copy(historyMetadata = action.historyMetadata) is HomeFragmentAction.SelectPocketStoriesCategory -> { - // Selecting a category means the stories to be displayed needs to also be changed. val updatedCategoriesState = state.copy( - pocketStoriesCategories = state.pocketStoriesCategories.map { - when (it.name == action.categoryName) { - true -> it.copy(isSelected = true, lastInteractedWithTimestamp = System.currentTimeMillis()) - false -> it - } - } + pocketStoriesCategoriesSelections = + state.pocketStoriesCategoriesSelections + PocketRecommendedStoriesSelectedCategory( + name = action.categoryName + ) ) + + // Selecting a category means the stories to be displayed needs to also be changed. return updatedCategoriesState.copy( pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) ) } is HomeFragmentAction.DeselectPocketStoriesCategory -> { val updatedCategoriesState = state.copy( - // Deselecting a category means the stories to be displayed needs to also be changed. - pocketStoriesCategories = state.pocketStoriesCategories.map { - when (it.name == action.categoryName) { - true -> it.copy(isSelected = false, lastInteractedWithTimestamp = System.currentTimeMillis()) - false -> it - } + pocketStoriesCategoriesSelections = state.pocketStoriesCategoriesSelections.filterNot { + it.name == action.categoryName } ) + + // Deselecting a category means the stories to be displayed needs to also be changed. return updatedCategoriesState.copy( pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 0a38a95a38..2963da5967 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -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.recenttabs.controller.RecentTabController 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.PocketStoriesInteractor @@ -391,7 +391,7 @@ class SessionControlInteractor( controller.handleCustomizeHomeTapped() } - override fun onCategoryClick(categoryClicked: PocketRecommendedStoryCategory) { + override fun onCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) { pocketStoriesController.handleCategoryClick(categoryClicked) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoryCategory.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesCategory.kt similarity index 58% rename from app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoryCategory.kt rename to app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesCategory.kt index f141bf0394..c7f99651cf 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoryCategory.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesCategory.kt @@ -13,18 +13,15 @@ import mozilla.components.service.pocket.PocketRecommendedStory 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. * * @property name The exact name of each category. Case sensitive. - * @property stories All [PocketRecommendedStory]es 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. + * @property stories All [PocketRecommendedStory]s with this category. */ -data class PocketRecommendedStoryCategory( +data class PocketRecommendedStoriesCategory( val name: String, - val stories: List = emptyList(), - val isSelected: Boolean = false, - val lastInteractedWithTimestamp: Long = 0L + val stories: List = emptyList() ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesSelectedCategory.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesSelectedCategory.kt new file mode 100644 index 0000000000..b016e43ad8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketRecommendedStoriesSelectedCategory.kt @@ -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() +) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt index f3ecd5756b..20b0569787 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt @@ -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 onCategoryClick Callback for when the user taps a category. */ @Composable fun PocketStoriesCategories( - categories: List, - onCategoryClick: (PocketRecommendedStoryCategory) -> Unit + categories: List, + selections: List, + onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit ) { StaggeredHorizontalGrid( horizontalItemsSpacing = 16.dp ) { 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) } } @@ -241,8 +242,9 @@ private fun PocketStoriesComposablesPreview() { PocketStoriesCategories( "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor".split(" ").map { - PocketRecommendedStoryCategory(it) - } + PocketRecommendedStoriesCategory(it) + }, + emptyList() ) { } Spacer(Modifier.height(10.dp)) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt index 56ee8c989f..627415e8ba 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt @@ -19,11 +19,11 @@ import org.mozilla.fenix.R */ 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. @@ -52,30 +52,20 @@ internal class DefaultPocketStoriesController( private val homeStore: HomeFragmentStore, private val navController: NavController ) : PocketStoriesController { - override fun handleCategoryClick(categoryClicked: PocketRecommendedStoryCategory) { - val allCategories = homeStore.state.pocketStoriesCategories + override fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) { + val initialCategoriesSelections = homeStore.state.pocketStoriesCategoriesSelections // 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)) return } // If a new category is clicked to be selected: // 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 = - if (currentlySelectedCategoriesCount == POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT) { - allCategories - .filter { it.isSelected } - .reduce { oldestSelected, category -> - when (oldestSelected.lastInteractedWithTimestamp <= category.lastInteractedWithTimestamp) { - true -> oldestSelected - false -> category - } - } + if (initialCategoriesSelections.size == POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT) { + initialCategoriesSelections.minByOrNull { it.selectionTimestamp } } else { null } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt index 7f4d18a591..67dab51372 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt @@ -13,9 +13,9 @@ interface PocketStoriesInteractor { /** * 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. diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt index f06b9a0dc9..88a58abb7a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt @@ -72,7 +72,7 @@ fun PocketStories( store: HomeFragmentStore, client: Client, onStoriesShown: (List) -> Unit, - onCategoryClick: (PocketRecommendedStoryCategory) -> Unit, + onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit, onExternalLinkClicked: (String) -> Unit ) { val stories = store @@ -81,6 +81,9 @@ fun PocketStories( val categories = store .observeAsComposableState { state -> state.pocketStoriesCategories }.value + val categoriesSelections = store + .observeAsComposableState { state -> state.pocketStoriesCategoriesSelections }.value + LaunchedEffect(stories) { // 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. @@ -109,7 +112,10 @@ fun PocketStories( Spacer(Modifier.height(17.dp)) - PocketStoriesCategories(categories ?: emptyList()) { + PocketStoriesCategories( + categories = categories ?: emptyList(), + selections = categoriesSelections ?: emptyList() + ) { onCategoryClick(it) } diff --git a/app/src/test/java/org/mozilla/fenix/ext/HomeFragmentStateTest.kt b/app/src/test/java/org/mozilla/fenix/ext/HomeFragmentStateTest.kt index 97f07c4059..ec20715a04 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/HomeFragmentStateTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/HomeFragmentStateTest.kt @@ -12,15 +12,16 @@ import org.junit.Assert.assertTrue import org.junit.Test 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.PocketRecommendedStoryCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory import kotlin.random.Random class HomeFragmentStateTest { private val otherStoriesCategory = - PocketRecommendedStoryCategory("other", getFakePocketStories(3, "other")) + PocketRecommendedStoriesCategory("other", getFakePocketStories(3, "other")) private val anotherStoriesCategory = - PocketRecommendedStoryCategory("another", getFakePocketStories(3, "another")) - private val defaultStoriesCategory = PocketRecommendedStoryCategory( + PocketRecommendedStoriesCategory("another", getFakePocketStories(3, "another")) + private val defaultStoriesCategory = PocketRecommendedStoriesCategory( POCKET_STORIES_DEFAULT_CATEGORY_NAME, getFakePocketStories(3) ) @@ -60,9 +61,8 @@ class HomeFragmentStateTest { @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`() { val homeState = HomeFragmentState( - pocketStoriesCategories = listOf( - otherStoriesCategory.copy(isSelected = true), anotherStoriesCategory, defaultStoriesCategory - ) + pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory), + pocketStoriesCategoriesSelections = listOf(PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name)) ) var result = homeState.getFilteredStories(2) @@ -77,10 +77,10 @@ class HomeFragmentStateTest { @Test 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( - pocketStoriesCategories = listOf( - otherStoriesCategory.copy(isSelected = true), - anotherStoriesCategory.copy(isSelected = true), - defaultStoriesCategory + pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory), + pocketStoriesCategoriesSelections = listOf( + PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name), + PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name) ) ) @@ -103,19 +103,19 @@ class HomeFragmentStateTest { @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`() { - val firstSelectedCategory = otherStoriesCategory.copy(lastInteractedWithTimestamp = 0, isSelected = true) - val lastSelectedCategory = anotherStoriesCategory.copy(lastInteractedWithTimestamp = 1, isSelected = true) val homeState = HomeFragmentState( - pocketStoriesCategories = listOf( - firstSelectedCategory, lastSelectedCategory, defaultStoriesCategory + pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory), + pocketStoriesCategoriesSelections = listOf( + PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name, selectionTimestamp = 0), + PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name, selectionTimestamp = 1) ) ) val result = homeState.getFilteredStories(5) assertEquals(5, result.size) - assertEquals(2, result.filter { it.category == firstSelectedCategory.name }.size) - assertEquals(3, result.filter { it.category == lastSelectedCategory.name }.size) + assertEquals(2, result.filter { it.category == otherStoriesCategory.name }.size) + assertEquals(3, result.filter { it.category == anotherStoriesCategory.name }.size) } @Test @@ -209,8 +209,8 @@ class HomeFragmentStateTest { @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`() { - val firstCategory = PocketRecommendedStoryCategory( - "first", getFakePocketStories(3, "first"), true, 0 + val firstCategory = PocketRecommendedStoriesCategory( + "first", getFakePocketStories(3, "first") ).run { // Avoid the first item also being the oldest to eliminate a potential bug in code // that would still get the expected result. @@ -224,8 +224,8 @@ class HomeFragmentStateTest { } ) } - val secondCategory = PocketRecommendedStoryCategory( - "second", getFakePocketStories(3, "second"), true, 222 + val secondCategory = PocketRecommendedStoriesCategory( + "second", getFakePocketStories(3, "second") ).run { // Avoid the first item also being the oldest to eliminate a potential bug in code // 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) diff --git a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt index fb9a5075ff..bfad8e3443 100644 --- a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt @@ -28,7 +28,8 @@ import org.mozilla.fenix.ext.getFilteredStories import org.mozilla.fenix.historymetadata.HistoryMetadataGroup import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_TO_SHOW_COUNT -import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoryCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory +import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.onboarding.FenixOnboarding class HomeFragmentStoreTest { @@ -189,8 +190,8 @@ class HomeFragmentStoreTest { @Test fun `Test selecting a Pocket recommendations category`() = runBlocking { - val otherStoriesCategory = PocketRecommendedStoryCategory("other") - val anotherStoriesCategory = PocketRecommendedStoryCategory("another") + val otherStoriesCategory = PocketRecommendedStoriesCategory("other") + val anotherStoriesCategory = PocketRecommendedStoriesCategory("another") val filteredStories = listOf(mockk()) homeFragmentStore = HomeFragmentStore( HomeFragmentState( @@ -208,7 +209,7 @@ class HomeFragmentStoreTest { verify { any().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } } - val selectedCategories = homeFragmentStore.state.pocketStoriesCategories.filter { it.isSelected } + val selectedCategories = homeFragmentStore.state.pocketStoriesCategoriesSelections assertEquals(1, selectedCategories.size) assertTrue(otherStoriesCategory.name === selectedCategories[0].name) assertSame(filteredStories, homeFragmentStore.state.pocketStories) @@ -216,13 +217,15 @@ class HomeFragmentStoreTest { @Test fun `Test deselecting a Pocket recommendations category`() = runBlocking { - val otherStoriesCategory = PocketRecommendedStoryCategory("other", isSelected = true) - val anotherStoriesCategory = PocketRecommendedStoryCategory("another", isSelected = true) + val otherStoriesCategory = PocketRecommendedStoriesCategory("other") + val anotherStoriesCategory = PocketRecommendedStoriesCategory("another") val filteredStories = listOf(mockk()) homeFragmentStore = HomeFragmentStore( HomeFragmentState( - pocketStoriesCategories = listOf( - otherStoriesCategory, anotherStoriesCategory + pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory), + pocketStoriesCategoriesSelections = listOf( + PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name), + PocketRecommendedStoriesSelectedCategory(anotherStoriesCategory.name) ) ) ) @@ -235,10 +238,9 @@ class HomeFragmentStoreTest { verify { any().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } } - assertTrue( - listOf(anotherStoriesCategory) - .containsAll(homeFragmentStore.state.pocketStoriesCategories.filter { it.isSelected }) - ) + val selectedCategories = homeFragmentStore.state.pocketStoriesCategoriesSelections + assertEquals(1, selectedCategories.size) + assertTrue(anotherStoriesCategory.name === selectedCategories[0].name) assertSame(filteredStories, homeFragmentStore.state.pocketStories) } @@ -259,8 +261,8 @@ class HomeFragmentStoreTest { @Test fun `Test updating the list of Pocket recommendations categories`() = runBlocking { - val otherStoriesCategory = PocketRecommendedStoryCategory("other") - val anotherStoriesCategory = PocketRecommendedStoryCategory("another", isSelected = true) + val otherStoriesCategory = PocketRecommendedStoriesCategory("other") + val anotherStoriesCategory = PocketRecommendedStoriesCategory("another") homeFragmentStore = HomeFragmentStore(HomeFragmentState()) mockkStatic("org.mozilla.fenix.ext.HomeFragmentStateKt") { @@ -268,9 +270,7 @@ class HomeFragmentStoreTest { every { any().getFilteredStories(any()) } returns firstFilteredStories homeFragmentStore.dispatch( - HomeFragmentAction.PocketStoriesCategoriesChange( - listOf(otherStoriesCategory, anotherStoriesCategory) - ) + HomeFragmentAction.PocketStoriesCategoriesChange(listOf(otherStoriesCategory, anotherStoriesCategory)) ).join() verify { any().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) } assertTrue( @@ -280,7 +280,7 @@ class HomeFragmentStoreTest { ) assertSame(firstFilteredStories, homeFragmentStore.state.pocketStories) - val updatedCategories = listOf(PocketRecommendedStoryCategory("yetAnother")) + val updatedCategories = listOf(PocketRecommendedStoriesCategory("yetAnother")) val secondFilteredStories = listOf(mockk()) every { any().getFilteredStories(any()) } returns secondFilteredStories homeFragmentStore.dispatch( diff --git a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index 62ed354bde..7be805ce99 100644 --- a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -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.sessioncontrol.DefaultSessionControlController 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 class SessionControlInteractorTest { @@ -215,7 +215,7 @@ class SessionControlInteractorTest { @Test 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) diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt index c1548663a9..ad4effd8f2 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt @@ -21,11 +21,15 @@ import org.mozilla.fenix.home.HomeFragmentStore class DefaultPocketStoriesControllerTest { @Test fun `GIVEN a category is selected WHEN that same category is clicked THEN deselect it`() { - val category1 = PocketRecommendedStoryCategory("cat1", emptyList(), isSelected = false) - val category2 = PocketRecommendedStoryCategory("cat2", emptyList(), isSelected = true) + val category1 = PocketRecommendedStoriesCategory("cat1", emptyList()) + val category2 = PocketRecommendedStoriesCategory("cat2", emptyList()) + val selections = listOf(PocketRecommendedStoriesSelectedCategory(category2.name)) val store = spyk( HomeFragmentStore( - HomeFragmentState(pocketStoriesCategories = listOf(category1, category2)) + HomeFragmentState( + pocketStoriesCategories = listOf(category1, category2), + pocketStoriesCategoriesSelections = selections + ) ) ) val controller = DefaultPocketStoriesController(mockk(), store, mockk()) @@ -39,23 +43,19 @@ class DefaultPocketStoriesControllerTest { @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`() { - val category1 = PocketRecommendedStoryCategory( - "cat1", emptyList(), isSelected = true, lastInteractedWithTimestamp = 111 - ) - val category2 = category1.copy("cat2", lastInteractedWithTimestamp = 222) - val category3 = category1.copy("cat3", lastInteractedWithTimestamp = 333) - val oldestSelectedCategory = category1.copy("oldestSelectedCategory", lastInteractedWithTimestamp = 0) - val category4 = category1.copy("cat4", lastInteractedWithTimestamp = 444) - val category5 = category1.copy("cat5", lastInteractedWithTimestamp = 555) - val category6 = category1.copy("cat6", lastInteractedWithTimestamp = 678) - val category7 = category1.copy("cat6", lastInteractedWithTimestamp = 890) - val newSelectedCategory = category1.copy( - "newSelectedCategory", isSelected = false, lastInteractedWithTimestamp = 654321 - ) + val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111) + val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222) + val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333) + val oldestSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "oldestSelectedCategory", selectionTimestamp = 0) + val category4 = PocketRecommendedStoriesSelectedCategory(name = "cat4", selectionTimestamp = 444) + val category5 = PocketRecommendedStoriesSelectedCategory(name = "cat5", selectionTimestamp = 555) + val category6 = PocketRecommendedStoriesSelectedCategory(name = "cat6", selectionTimestamp = 678) + val category7 = PocketRecommendedStoriesSelectedCategory(name = "cat7", selectionTimestamp = 890) + val newSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "newSelectedCategory", selectionTimestamp = 654321) val store = spyk( HomeFragmentStore( HomeFragmentState( - pocketStoriesCategories = listOf( + pocketStoriesCategoriesSelections = listOf( category1, category2, category3, category4, category5, category6, category7, oldestSelectedCategory ) ) @@ -63,7 +63,7 @@ class DefaultPocketStoriesControllerTest { ) 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.SelectPocketStoriesCategory(newSelectedCategory.name)) } @@ -71,22 +71,17 @@ class DefaultPocketStoriesControllerTest { @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`() { - val category1 = PocketRecommendedStoryCategory( - "cat1", emptyList(), isSelected = true, lastInteractedWithTimestamp = 111 - ) - val category2 = category1.copy("cat2", lastInteractedWithTimestamp = 222) - val category3 = category1.copy("cat3", lastInteractedWithTimestamp = 333) - val oldestSelectedCategory = category1.copy("oldestSelectedCategory", lastInteractedWithTimestamp = 0) - val category4 = category1.copy("cat4", lastInteractedWithTimestamp = 444) - val category5 = category1.copy("cat5", lastInteractedWithTimestamp = 555) - val category6 = category1.copy("cat6", lastInteractedWithTimestamp = 678) - val newSelectedCategory = category1.copy( - "newSelectedCategory", isSelected = false, lastInteractedWithTimestamp = 654321 - ) + val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111) + val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222) + val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333) + val oldestSelectedCategory = PocketRecommendedStoriesSelectedCategory(name = "oldestSelectedCategory", selectionTimestamp = 0) + val category4 = PocketRecommendedStoriesSelectedCategory(name = "cat4", selectionTimestamp = 444) + val category5 = PocketRecommendedStoriesSelectedCategory(name = "cat5", selectionTimestamp = 555) + val category6 = PocketRecommendedStoriesSelectedCategory(name = "cat6", selectionTimestamp = 678) val store = spyk( HomeFragmentStore( HomeFragmentState( - pocketStoriesCategories = listOf( + pocketStoriesCategoriesSelections = listOf( category1, category2, category3, category4, category5, category6, oldestSelectedCategory ) ) @@ -94,10 +89,10 @@ class DefaultPocketStoriesControllerTest { ) val controller = DefaultPocketStoriesController(mockk(), store, mockk()) - controller.handleCategoryClick(newSelectedCategory) + controller.handleCategoryClick(PocketRecommendedStoriesCategory("newSelectedCategory")) verify(exactly = 0) { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) } - verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategory.name)) } + verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory("newSelectedCategory")) } } @Test