For #21593 - Persist stories categories selections in a Proto DataStore
A fast and easy solution with all the ACID requirements. Also supports easy migrations if later the data we need persisted changes.
This commit is contained in:
parent
565beb88c9
commit
e4489b8d7d
|
@ -1,5 +1,6 @@
|
|||
plugins {
|
||||
id "com.jetbrains.python.envs" version "0.0.26"
|
||||
id "com.google.protobuf" version "0.8.17"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
@ -525,6 +526,8 @@ dependencies {
|
|||
implementation Deps.androidx_core_ktx
|
||||
implementation Deps.androidx_transition
|
||||
implementation Deps.androidx_work_ktx
|
||||
implementation Deps.androidx_datastore
|
||||
implementation Deps.protobuf_javalite
|
||||
implementation Deps.google_material
|
||||
|
||||
implementation Deps.adjust
|
||||
|
@ -589,6 +592,25 @@ dependencies {
|
|||
lintChecks project(":mozilla-lint-rules")
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = Deps.protobuf_compiler
|
||||
}
|
||||
|
||||
// Generates the java Protobuf-lite code for the Protobufs in this project. See
|
||||
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
|
||||
// for more information.
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option 'lite'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty("coverage")) {
|
||||
tasks.withType(Test).configureEach {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/* 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.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.dataStore
|
||||
|
||||
/**
|
||||
* Application / process unique [DataStore] for IO operations related to Pocket recommended stories selected categories.
|
||||
*/
|
||||
internal val Context.pocketStoriesSelectedCategoriesDataStore: DataStore<SelectedPocketStoriesCategories> by dataStore(
|
||||
fileName = "pocket_recommendations_selected_categories.pb",
|
||||
serializer = SelectedPocketStoriesCategorySerializer
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
/* 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.datastore
|
||||
|
||||
import androidx.datastore.core.Serializer
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for [SelectedPocketStoriesCategories] defined in selected_pocket_stories_categories.proto.
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object SelectedPocketStoriesCategorySerializer : Serializer<SelectedPocketStoriesCategories> {
|
||||
override val defaultValue: SelectedPocketStoriesCategories = SelectedPocketStoriesCategories.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): SelectedPocketStoriesCategories {
|
||||
return SelectedPocketStoriesCategories.parseFrom(input)
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: SelectedPocketStoriesCategories, output: OutputStream) {
|
||||
t.writeTo(output)
|
||||
}
|
||||
}
|
|
@ -93,6 +93,7 @@ import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
|
|||
import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
|
||||
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||
import org.mozilla.fenix.databinding.FragmentHomeBinding
|
||||
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
|
||||
import org.mozilla.fenix.ext.asRecentTabs
|
||||
import org.mozilla.fenix.experiments.FeatureId
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
@ -248,7 +249,9 @@ class HomeFragment : Fragment() {
|
|||
),
|
||||
listOf(
|
||||
PocketUpdatesMiddleware(
|
||||
lifecycleScope, requireComponents.core.pocketStoriesService
|
||||
lifecycleScope,
|
||||
requireComponents.core.pocketStoriesService,
|
||||
requireContext().pocketStoriesSelectedCategoriesDataStore
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -103,6 +103,10 @@ sealed class HomeFragmentAction : Action {
|
|||
data class PocketStoriesChange(val pocketStories: List<PocketRecommendedStory>) : HomeFragmentAction()
|
||||
data class PocketStoriesCategoriesChange(val storiesCategories: List<PocketRecommendedStoriesCategory>) :
|
||||
HomeFragmentAction()
|
||||
data class PocketStoriesCategoriesSelectionsChange(
|
||||
val storiesCategories: List<PocketRecommendedStoriesCategory>,
|
||||
val categoriesSelected: List<PocketRecommendedStoriesSelectedCategory>
|
||||
) : HomeFragmentAction()
|
||||
object RemoveCollectionsPlaceholder : HomeFragmentAction()
|
||||
object RemoveSetDefaultBrowserCard : HomeFragmentAction()
|
||||
}
|
||||
|
@ -172,8 +176,18 @@ private fun homeFragmentStateReducer(
|
|||
)
|
||||
}
|
||||
is HomeFragmentAction.PocketStoriesCategoriesChange -> {
|
||||
// Whenever categories change stories to be displayed needs to also be changed.
|
||||
val updatedCategoriesState = state.copy(pocketStoriesCategories = action.storiesCategories)
|
||||
// Whenever categories change stories to be displayed needs to also be changed.
|
||||
return updatedCategoriesState.copy(
|
||||
pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT)
|
||||
)
|
||||
}
|
||||
is HomeFragmentAction.PocketStoriesCategoriesSelectionsChange -> {
|
||||
val updatedCategoriesState = state.copy(
|
||||
pocketStoriesCategories = action.storiesCategories,
|
||||
pocketStoriesCategoriesSelections = action.categoriesSelected
|
||||
)
|
||||
// Whenever categories change stories to be displayed needs to also be changed.
|
||||
return updatedCategoriesState.copy(
|
||||
pocketStories = updatedCategoriesState.getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT)
|
||||
)
|
||||
|
|
|
@ -4,25 +4,57 @@
|
|||
|
||||
package org.mozilla.fenix.home
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.service.pocket.PocketStoriesService
|
||||
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories
|
||||
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories.SelectedPocketStoriesCategory
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
|
||||
|
||||
/**
|
||||
* [HomeFragmentStore] middleware reacting in response to Pocket related [Action]s.
|
||||
*
|
||||
* @param coroutineScope [CoroutineScope] used for long running operations like disk IO.
|
||||
* @param pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories.
|
||||
* @param selectedPocketCategoriesDataStore [DataStore] used for reading or persisting details about the
|
||||
* currently selected Pocket recommended stories categories.
|
||||
*/
|
||||
class PocketUpdatesMiddleware(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val pocketStoriesService: PocketStoriesService
|
||||
private val pocketStoriesService: PocketStoriesService,
|
||||
private val selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>
|
||||
) : Middleware<HomeFragmentState, HomeFragmentAction> {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<HomeFragmentState, HomeFragmentAction>,
|
||||
next: (HomeFragmentAction) -> Unit,
|
||||
action: HomeFragmentAction
|
||||
) {
|
||||
// Pre process actions
|
||||
when (action) {
|
||||
is HomeFragmentAction.PocketStoriesCategoriesChange -> {
|
||||
// Intercept the original action which would only update categories and
|
||||
// dispatch a new action which also updates which categories are selected by the user
|
||||
// from previous locally persisted data.
|
||||
restoreSelectedCategories(
|
||||
coroutineScope = coroutineScope,
|
||||
currentCategories = action.storiesCategories,
|
||||
store = context.store,
|
||||
selectedPocketCategoriesDataStore = selectedPocketCategoriesDataStore
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
next(action)
|
||||
|
||||
// Post process actions
|
||||
|
@ -36,9 +68,80 @@ class PocketUpdatesMiddleware(
|
|||
)
|
||||
}
|
||||
}
|
||||
is HomeFragmentAction.SelectPocketStoriesCategory,
|
||||
is HomeFragmentAction.DeselectPocketStoriesCategory -> {
|
||||
persistSelectedCategories(
|
||||
coroutineScope = coroutineScope,
|
||||
currentCategoriesSelections = context.state.pocketStoriesCategoriesSelections,
|
||||
selectedPocketCategoriesDataStore = selectedPocketCategoriesDataStore
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist [currentCategoriesSelections] for making this details available in between app restarts.
|
||||
*
|
||||
* @param coroutineScope [CoroutineScope] used for reading the locally persisted data.
|
||||
* @param currentCategoriesSelections Currently selected Pocket recommended stories categories.
|
||||
* @param selectedPocketCategoriesDataStore - DataStore used for persisting [currentCategoriesSelections].
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun persistSelectedCategories(
|
||||
coroutineScope: CoroutineScope,
|
||||
currentCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory>,
|
||||
selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>
|
||||
) {
|
||||
val selectedCategories = currentCategoriesSelections
|
||||
.map {
|
||||
SelectedPocketStoriesCategory.newBuilder().apply {
|
||||
name = it.name
|
||||
selectionTimestamp = it.selectionTimestamp
|
||||
}.build()
|
||||
}
|
||||
|
||||
// Irrespective of the current selections or their number overwrite everything we had.
|
||||
coroutineScope.launch {
|
||||
selectedPocketCategoriesDataStore.updateData { data ->
|
||||
data.newBuilderForType().addAllValues(selectedCategories).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines [currentCategories] with the locally persisted data about previously selected categories
|
||||
* and emits a new [HomeFragmentAction.PocketStoriesCategoriesSelectionsChange] to update these in store.
|
||||
*
|
||||
* @param coroutineScope [CoroutineScope] used for reading the locally persisted data.
|
||||
* @param currentCategories Stories categories currently available
|
||||
* @param store [Store] that will be updated.
|
||||
* @param selectedPocketCategoriesDataStore [DataStore] containing details about the previously selected
|
||||
* stories categories.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun restoreSelectedCategories(
|
||||
coroutineScope: CoroutineScope,
|
||||
currentCategories: List<PocketRecommendedStoriesCategory>,
|
||||
store: Store<HomeFragmentState, HomeFragmentAction>,
|
||||
selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
selectedPocketCategoriesDataStore.data.collect { persistedSelectedCategories ->
|
||||
store.dispatch(
|
||||
HomeFragmentAction.PocketStoriesCategoriesSelectionsChange(
|
||||
currentCategories,
|
||||
persistedSelectedCategories.valuesList.map {
|
||||
PocketRecommendedStoriesSelectedCategory(
|
||||
name = it.name,
|
||||
selectionTimestamp = it.selectionTimestamp
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* 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/. */
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package proto;
|
||||
|
||||
option java_package = "org.mozilla.fenix.datastore";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// List of currently selected Pocket recommended stories categories.
|
||||
message SelectedPocketStoriesCategories {
|
||||
|
||||
// Details about a selected Pocket recommended stories category.
|
||||
// See [org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory]
|
||||
message SelectedPocketStoriesCategory {
|
||||
// Name of this category.
|
||||
string name = 1;
|
||||
// Timestamp for when this category was selected.
|
||||
int64 selectionTimestamp = 2;
|
||||
}
|
||||
|
||||
// Currently selected Pocket stories categories.
|
||||
repeated SelectedPocketStoriesCategory values = 1;
|
||||
}
|
|
@ -195,8 +195,9 @@ class HomeFragmentStoreTest {
|
|||
val filteredStories = listOf(mockk<PocketRecommendedStory>())
|
||||
homeFragmentStore = HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
pocketStoriesCategories = listOf(
|
||||
otherStoriesCategory, anotherStoriesCategory
|
||||
pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory),
|
||||
pocketStoriesCategoriesSelections = listOf(
|
||||
PocketRecommendedStoriesSelectedCategory(otherStoriesCategory.name),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -204,13 +205,13 @@ class HomeFragmentStoreTest {
|
|||
mockkStatic("org.mozilla.fenix.ext.HomeFragmentStateKt") {
|
||||
every { any<HomeFragmentState>().getFilteredStories(any()) } returns filteredStories
|
||||
|
||||
homeFragmentStore.dispatch(HomeFragmentAction.SelectPocketStoriesCategory("other")).join()
|
||||
homeFragmentStore.dispatch(HomeFragmentAction.SelectPocketStoriesCategory("another")).join()
|
||||
|
||||
verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) }
|
||||
}
|
||||
|
||||
val selectedCategories = homeFragmentStore.state.pocketStoriesCategoriesSelections
|
||||
assertEquals(1, selectedCategories.size)
|
||||
assertEquals(2, selectedCategories.size)
|
||||
assertTrue(otherStoriesCategory.name === selectedCategories[0].name)
|
||||
assertSame(filteredStories, homeFragmentStore.state.pocketStories)
|
||||
}
|
||||
|
@ -293,4 +294,34 @@ class HomeFragmentStoreTest {
|
|||
assertSame(secondFilteredStories, homeFragmentStore.state.pocketStories)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test updating the list of selected Pocket recommendations categories`() = runBlocking {
|
||||
val otherStoriesCategory = PocketRecommendedStoriesCategory("other")
|
||||
val anotherStoriesCategory = PocketRecommendedStoriesCategory("another")
|
||||
val selectedCategory = PocketRecommendedStoriesSelectedCategory("selected")
|
||||
homeFragmentStore = HomeFragmentStore(HomeFragmentState())
|
||||
|
||||
mockkStatic("org.mozilla.fenix.ext.HomeFragmentStateKt") {
|
||||
val firstFilteredStories = listOf(mockk<PocketRecommendedStory>())
|
||||
every { any<HomeFragmentState>().getFilteredStories(any()) } returns firstFilteredStories
|
||||
|
||||
homeFragmentStore.dispatch(
|
||||
HomeFragmentAction.PocketStoriesCategoriesSelectionsChange(
|
||||
storiesCategories = listOf(otherStoriesCategory, anotherStoriesCategory),
|
||||
categoriesSelected = listOf(selectedCategory)
|
||||
)
|
||||
).join()
|
||||
verify { any<HomeFragmentState>().getFilteredStories(POCKET_STORIES_TO_SHOW_COUNT) }
|
||||
assertTrue(
|
||||
homeFragmentStore.state.pocketStoriesCategories.containsAll(
|
||||
listOf(otherStoriesCategory, anotherStoriesCategory)
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
homeFragmentStore.state.pocketStoriesCategoriesSelections.containsAll(listOf(selectedCategory))
|
||||
)
|
||||
assertSame(firstFilteredStories, homeFragmentStore.state.pocketStories)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,26 @@
|
|||
|
||||
package org.mozilla.fenix.home
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import mozilla.components.service.pocket.PocketRecommendedStory
|
||||
import mozilla.components.service.pocket.PocketStoriesService
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories
|
||||
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories.SelectedPocketStoriesCategory
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class PocketUpdatesMiddlewareTest {
|
||||
@ExperimentalCoroutinesApi
|
||||
@Test
|
||||
fun `WHEN PocketStoriesShown is dispatched THEN update PocketStoriesService`() {
|
||||
val story1 = PocketRecommendedStory("title", "url1", "imageUrl", "publisher", "category", 0, timesShown = 0)
|
||||
|
@ -22,7 +31,7 @@ class PocketUpdatesMiddlewareTest {
|
|||
val story3 = story1.copy("title3", "url3")
|
||||
val coroutineScope = TestCoroutineScope()
|
||||
val pocketService: PocketStoriesService = mockk(relaxed = true)
|
||||
val pocketMiddleware = PocketUpdatesMiddleware(coroutineScope, pocketService)
|
||||
val pocketMiddleware = PocketUpdatesMiddleware(coroutineScope, pocketService, mockk())
|
||||
val homeStore = HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
pocketStories = listOf(story1, story2, story3)
|
||||
|
@ -34,4 +43,129 @@ class PocketUpdatesMiddlewareTest {
|
|||
|
||||
coVerify { pocketService.updateStoriesTimesShown(listOf(story2.copy(timesShown = 1))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN PocketStoriesCategoriesChange is dispatched THEN intercept and dispatch PocketStoriesCategoriesSelectionsChange`() {
|
||||
val persistedSelectedCategory: SelectedPocketStoriesCategory = mockk {
|
||||
every { name } returns "testCategory"
|
||||
every { selectionTimestamp } returns 123
|
||||
}
|
||||
val persistedSelectedCategories: SelectedPocketStoriesCategories = mockk {
|
||||
every { valuesList } returns mutableListOf(persistedSelectedCategory)
|
||||
}
|
||||
val dataStore: DataStore<SelectedPocketStoriesCategories> = mockk {
|
||||
every { data } returns flowOf(persistedSelectedCategories)
|
||||
}
|
||||
val currentCategories = listOf(mockk<PocketRecommendedStoriesCategory>())
|
||||
val pocketMiddleware = PocketUpdatesMiddleware(TestCoroutineScope(), mockk(), dataStore)
|
||||
val homeStore = spyk(
|
||||
HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
pocketStoriesCategories = currentCategories
|
||||
),
|
||||
listOf(pocketMiddleware)
|
||||
)
|
||||
)
|
||||
|
||||
homeStore.dispatch(HomeFragmentAction.PocketStoriesCategoriesChange(currentCategories)).joinBlocking()
|
||||
|
||||
verify {
|
||||
homeStore.dispatch(
|
||||
HomeFragmentAction.PocketStoriesCategoriesSelectionsChange(
|
||||
storiesCategories = currentCategories,
|
||||
categoriesSelected = listOf(
|
||||
PocketRecommendedStoriesSelectedCategory("testCategory", 123)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN SelectPocketStoriesCategory is dispatched THEN persist details in DataStore`() {
|
||||
val categ1 = PocketRecommendedStoriesCategory("categ1")
|
||||
val categ2 = PocketRecommendedStoriesCategory("categ2")
|
||||
val dataStore: DataStore<SelectedPocketStoriesCategories> = mockk(relaxed = true)
|
||||
val pocketMiddleware = PocketUpdatesMiddleware(TestCoroutineScope(), mockk(), dataStore)
|
||||
val homeStore = spyk(
|
||||
HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
pocketStoriesCategories = listOf(categ1, categ2)
|
||||
),
|
||||
listOf(pocketMiddleware)
|
||||
)
|
||||
)
|
||||
|
||||
homeStore.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(categ2.name)).joinBlocking()
|
||||
|
||||
// Seems like the most we can test is that an update was made.
|
||||
coVerify { dataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN DeselectPocketStoriesCategory is dispatched THEN persist details in DataStore`() {
|
||||
val categ1 = PocketRecommendedStoriesCategory("categ1")
|
||||
val categ2 = PocketRecommendedStoriesCategory("categ2")
|
||||
val dataStore: DataStore<SelectedPocketStoriesCategories> = mockk(relaxed = true)
|
||||
val pocketMiddleware = PocketUpdatesMiddleware(TestCoroutineScope(), mockk(), dataStore)
|
||||
val homeStore = spyk(
|
||||
HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
pocketStoriesCategories = listOf(categ1, categ2)
|
||||
),
|
||||
listOf(pocketMiddleware)
|
||||
)
|
||||
)
|
||||
|
||||
homeStore.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(categ2.name)).joinBlocking()
|
||||
|
||||
// Seems like the most we can test is that an update was made.
|
||||
coVerify { dataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN persistCategories is called THEN update dataStore`() {
|
||||
val dataStore: DataStore<SelectedPocketStoriesCategories> = mockk(relaxed = true)
|
||||
|
||||
persistSelectedCategories(TestCoroutineScope(), listOf(mockk(relaxed = true)), dataStore)
|
||||
|
||||
// Seems like the most we can test is that an update was made.
|
||||
coVerify { dataStore.updateData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN restoreSelectedCategories is called THEN dispatch PocketStoriesCategoriesSelectionsChange with data read from the persistence layer`() {
|
||||
val persistedSelectedCategory: SelectedPocketStoriesCategory = mockk {
|
||||
every { name } returns "testCategory"
|
||||
every { selectionTimestamp } returns 123
|
||||
}
|
||||
val persistedSelectedCategories: SelectedPocketStoriesCategories = mockk {
|
||||
every { valuesList } returns mutableListOf(persistedSelectedCategory)
|
||||
}
|
||||
val dataStore: DataStore<SelectedPocketStoriesCategories> = mockk {
|
||||
every { data } returns flowOf(persistedSelectedCategories)
|
||||
}
|
||||
val currentCategories = listOf(mockk<PocketRecommendedStoriesCategory>())
|
||||
val homeStore = spyk(
|
||||
HomeFragmentStore(HomeFragmentState())
|
||||
)
|
||||
|
||||
restoreSelectedCategories(
|
||||
coroutineScope = TestCoroutineScope(),
|
||||
currentCategories = currentCategories,
|
||||
store = homeStore,
|
||||
selectedPocketCategoriesDataStore = dataStore
|
||||
)
|
||||
|
||||
coVerify {
|
||||
homeStore.dispatch(
|
||||
HomeFragmentAction.PocketStoriesCategoriesSelectionsChange(
|
||||
storiesCategories = currentCategories,
|
||||
categoriesSelected = listOf(
|
||||
PocketRecommendedStoriesSelectedCategory("testCategory", 123)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ object Versions {
|
|||
const val androidx_paging = "2.1.2"
|
||||
const val androidx_transition = "1.4.0"
|
||||
const val androidx_work = "2.5.0"
|
||||
const val androidx_datastore = "1.0.0"
|
||||
const val google_material = "1.2.1"
|
||||
|
||||
const val mozilla_android_components = AndroidComponents.VERSION
|
||||
|
@ -52,6 +53,8 @@ object Versions {
|
|||
const val google_ads_id_version = "16.0.0"
|
||||
|
||||
const val google_play_store_version = "1.8.0"
|
||||
|
||||
const val protobuf = "3.11.4" // keep in sync with the version used in AS.
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
@ -199,8 +202,12 @@ object Deps {
|
|||
const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"
|
||||
const val androidx_work_ktx = "androidx.work:work-runtime-ktx:${Versions.androidx_work}"
|
||||
const val androidx_work_testing = "androidx.work:work-testing:${Versions.androidx_work}"
|
||||
const val androidx_datastore = "androidx.datastore:datastore:${Versions.androidx_datastore}"
|
||||
const val google_material = "com.google.android.material:material:${Versions.google_material}"
|
||||
|
||||
const val protobuf_javalite = "com.google.protobuf:protobuf-javalite:${Versions.protobuf}"
|
||||
const val protobuf_compiler = "com.google.protobuf:protoc:${Versions.protobuf}"
|
||||
|
||||
const val adjust = "com.adjust.sdk:adjust-android:${Versions.adjust}"
|
||||
const val installreferrer = "com.android.installreferrer:installreferrer:${Versions.installreferrer}"
|
||||
|
||||
|
|
Loading…
Reference in New Issue