From 9b4d26a93d4cba4be2a3976398b9d334ecfa8dfd Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 31 Mar 2022 13:37:37 -0700 Subject: [PATCH] Add loading indicator while for you data is loading Updates the for you view model to support displaying a loading state when data hasn't loaded yet. This is a somewhat involved CL, because there are multiple ways in which data could be loading: We could have topics, but the feed itself could be loading after we selected a new set of topics. Change-Id: I8662140c7132b195f85e69fee8e18745829ae975 --- .../feature/foryou/ForYouScreenTest.kt | 117 +- .../feature/foryou/FollowedInterestsState.kt | 41 + .../feature/foryou/ForYouFeedState.kt | 39 + .../foryou/ForYouInterestsSelectionState.kt | 49 + .../feature/foryou/ForYouScreen.kt | 326 ++-- .../feature/foryou/ForYouViewModel.kt | 232 ++- .../feature/foryou/ForYouViewModelTest.kt | 1521 +++++++++++------ 7 files changed, 1523 insertions(+), 802 deletions(-) create mode 100644 feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsState.kt create mode 100644 feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouFeedState.kt create mode 100644 feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionState.kt diff --git a/feature-foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature-foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index d7c730e2a..b36d79a9d 100644 --- a/feature-foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature-foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsOff -import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasScrollToNodeAction import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -51,7 +51,8 @@ class ForYouScreenTest { fun circularProgressIndicator_whenScreenIsLoading_exists() { composeTestRule.setContent { ForYouScreen( - uiState = ForYouFeedUiState.Loading, + interestsSelectionState = ForYouInterestsSelectionState.Loading, + feedState = ForYouFeedState.Loading, onAuthorCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -70,7 +71,7 @@ class ForYouScreenTest { fun topicSelector_whenNoTopicsSelected_showsTopicChipsAndDisabledDoneButton() { composeTestRule.setContent { ForYouScreen( - uiState = ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( + interestsSelectionState = ForYouInterestsSelectionState.WithInterestsSelection( topics = listOf( FollowableTopic( topic = Topic( @@ -127,7 +128,9 @@ class ForYouScreenTest { ), isFollowed = false ), - ), + ) + ), + feedState = ForYouFeedState.Success( feed = emptyList() ), onAuthorCheckedChanged = { _, _ -> }, @@ -169,7 +172,7 @@ class ForYouScreenTest { fun topicSelector_whenSomeTopicsSelected_showsTopicChipsAndEnabledDoneButton() { composeTestRule.setContent { ForYouScreen( - uiState = ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( + interestsSelectionState = ForYouInterestsSelectionState.WithInterestsSelection( topics = listOf( FollowableTopic( topic = Topic( @@ -227,6 +230,8 @@ class ForYouScreenTest { isFollowed = false ), ), + ), + feedState = ForYouFeedState.Success( feed = emptyList() ), onAuthorCheckedChanged = { _, _ -> }, @@ -274,7 +279,7 @@ class ForYouScreenTest { fun topicSelector_whenSomeAuthorsSelected_showsTopicChipsAndEnabledDoneButton() { composeTestRule.setContent { ForYouScreen( - uiState = ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( + interestsSelectionState = ForYouInterestsSelectionState.WithInterestsSelection( topics = listOf( FollowableTopic( topic = Topic( @@ -296,7 +301,7 @@ class ForYouScreenTest { url = "", imageUrl = "" ), - isFollowed = false + isFollowed = true ), FollowableTopic( topic = Topic( @@ -319,7 +324,7 @@ class ForYouScreenTest { twitter = "", mediumPage = "" ), - isFollowed = true + isFollowed = false ), FollowableAuthor( author = Author( @@ -332,6 +337,8 @@ class ForYouScreenTest { isFollowed = false ), ), + ), + feedState = ForYouFeedState.Success( feed = emptyList() ), onAuthorCheckedChanged = { _, _ -> }, @@ -359,12 +366,6 @@ class ForYouScreenTest { composeTestRule .onNodeWithText("Android Dev") .assertIsDisplayed() - .assertIsOn() - .assertHasClickAction() - - composeTestRule - .onNodeWithText("Android Dev 2") - .assertIsDisplayed() .assertIsOff() .assertHasClickAction() @@ -380,4 +381,92 @@ class ForYouScreenTest { .assertIsEnabled() .assertHasClickAction() } + + @Test + fun feed_whenInterestsSelectedAndLoading_showsLoadingIndicator() { + composeTestRule.setContent { + ForYouScreen( + interestsSelectionState = ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ), + isFollowed = false + ), + ), + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = true + ), + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + ), + ), + feedState = ForYouFeedState.Loading, + onAuthorCheckedChanged = { _, _ -> }, + onTopicCheckedChanged = { _, _ -> }, + saveFollowedTopics = {}, + onNewsResourcesCheckedChanged = { _, _ -> } + ) + } + + // Scroll until the loading indicator is visible + composeTestRule + .onAllNodes(hasScrollToNodeAction()) + .onFirst() + .performScrollToNode( + hasContentDescription( + composeTestRule.activity.resources.getString(R.string.for_you_loading) + ) + ) + + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.resources.getString(R.string.for_you_loading) + ) + .assertExists() + } } diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsState.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsState.kt new file mode 100644 index 000000000..61088d7c6 --- /dev/null +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/FollowedInterestsState.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.foryou + +/** + * A sealed hierarchy for the user's current followed interests state. + */ +sealed interface FollowedInterestsState { + + /** + * The current state is unknown (hasn't loaded yet) + */ + object Unknown : FollowedInterestsState + + /** + * The user hasn't followed any interests yet. + */ + object None : FollowedInterestsState + + /** + * The user has followed the given (non-empty) set of [topicIds] or [authorIds]. + */ + data class FollowedInterests( + val topicIds: Set, + val authorIds: Set + ) : FollowedInterestsState +} diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouFeedState.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouFeedState.kt new file mode 100644 index 000000000..3d19cc5a3 --- /dev/null +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouFeedState.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.foryou + +import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource + +/** + * A sealed hierarchy describing the state of the feed on the for you screen. + */ +sealed interface ForYouFeedState { + /** + * The feed is still loading. + */ + object Loading : ForYouFeedState + + /** + * The feed is loaded with the given list of news resources. + */ + data class Success( + /** + * The list of news resources contained in this [PopulatedFeed]. + */ + val feed: List + ) : ForYouFeedState +} diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionState.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionState.kt new file mode 100644 index 000000000..bca006d5a --- /dev/null +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouInterestsSelectionState.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.foryou + +import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor +import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic + +/** + * A sealed hierarchy describing the interests selection state for the for you screen. + */ +sealed interface ForYouInterestsSelectionState { + /** + * The interests selection state is loading. + */ + object Loading : ForYouInterestsSelectionState + + /** + * There is no interests selection state. + */ + object NoInterestsSelection : ForYouInterestsSelectionState + + /** + * There is a interests selection state, with the given lists of topics and authors. + */ + data class WithInterestsSelection( + val topics: List, + val authors: List + ) : ForYouInterestsSelectionState { + /** + * True if the current in-progress selection can be saved. + */ + val canSaveInterests: Boolean get() = + topics.any { it.isFollowed } || authors.any { it.isFollowed } + } +} diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 9047b4741..477e05e13 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells.Fixed import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid @@ -76,9 +77,6 @@ import com.google.samples.apps.nowinandroid.core.ui.component.NiaToggleButton import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection import kotlinx.datetime.Instant @Composable @@ -86,10 +84,12 @@ fun ForYouRoute( modifier: Modifier = Modifier, viewModel: ForYouViewModel = hiltViewModel() ) { - val uiState by viewModel.uiState.collectAsState() + val interestsSelectionState by viewModel.interestsSelectionState.collectAsState() + val feedState by viewModel.feedState.collectAsState() ForYouScreen( modifier = modifier, - uiState = uiState, + interestsSelectionState = interestsSelectionState, + feedState = feedState, onTopicCheckedChanged = viewModel::updateTopicSelection, onAuthorCheckedChanged = viewModel::updateAuthorSelection, saveFollowedTopics = viewModel::saveFollowedInterests, @@ -99,7 +99,8 @@ fun ForYouRoute( @Composable fun ForYouScreen( - uiState: ForYouFeedUiState, + interestsSelectionState: ForYouInterestsSelectionState, + feedState: ForYouFeedState, onTopicCheckedChanged: (String, Boolean) -> Unit, onAuthorCheckedChanged: (String, Boolean) -> Unit, saveFollowedTopics: () -> Unit, @@ -132,74 +133,91 @@ fun ForYouScreen( ) ) } - when (uiState) { - is ForYouFeedUiState.Loading -> { + + when (interestsSelectionState) { + ForYouInterestsSelectionState.Loading -> { item { LoadingWheel( - modifier = modifier, + modifier = Modifier + .fillMaxWidth() + .wrapContentSize(), contentDesc = stringResource(id = R.string.for_you_loading), ) } } - is PopulatedFeed -> { - when (uiState) { - is FeedWithInterestsSelection -> { - item { - Text( - text = stringResource(R.string.onboarding_guidance_title), - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), - style = NiaTypography.titleMedium - ) - } - item { - Text( - text = stringResource(R.string.onboarding_guidance_subtitle), - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, start = 16.dp, end = 16.dp), - textAlign = TextAlign.Center, - style = NiaTypography.bodyMedium - ) - } - item { - AuthorsCarousel( - authors = uiState.authors, - onAuthorClick = onAuthorCheckedChanged, - modifier = Modifier.padding(vertical = 8.dp) - ) - } - item { - TopicSelection( - uiState, - onTopicCheckedChanged, - Modifier.padding(bottom = 8.dp) - ) - } - item { - // Done button - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() - ) { - Button( - onClick = saveFollowedTopics, - enabled = uiState.canSaveInterests, - modifier = Modifier - .padding(horizontal = 40.dp) - .width(364.dp) - ) { - Text(text = stringResource(R.string.done)) - } - } + ForYouInterestsSelectionState.NoInterestsSelection -> Unit + is ForYouInterestsSelectionState.WithInterestsSelection -> { + item { + Text( + text = stringResource(R.string.onboarding_guidance_title), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + style = NiaTypography.titleMedium + ) + } + item { + Text( + text = stringResource(R.string.onboarding_guidance_subtitle), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, start = 16.dp, end = 16.dp), + textAlign = TextAlign.Center, + style = NiaTypography.bodyMedium + ) + } + item { + AuthorsCarousel( + authors = interestsSelectionState.authors, + onAuthorClick = onAuthorCheckedChanged, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + item { + TopicSelection( + interestsSelectionState, + onTopicCheckedChanged, + Modifier.padding(bottom = 8.dp) + ) + } + item { + // Done button + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = saveFollowedTopics, + enabled = interestsSelectionState.canSaveInterests, + modifier = Modifier + .padding(horizontal = 40.dp) + .width(364.dp) + ) { + Text(text = stringResource(R.string.done)) } } - is FeedWithoutTopicSelection -> Unit } + } + } - items(uiState.feed) { (newsResource: NewsResource, isBookmarked: Boolean) -> + when (feedState) { + ForYouFeedState.Loading -> { + // Avoid showing a second loading wheel if we already are for the interests + // selection + if (interestsSelectionState !is ForYouInterestsSelectionState.Loading) { + item { + LoadingWheel( + modifier = Modifier + .fillMaxWidth() + .wrapContentSize(), + contentDesc = stringResource(id = R.string.for_you_loading), + ) + } + } + } + is ForYouFeedState.Success -> { + items(feedState.feed) { (newsResource: NewsResource, isBookmarked: Boolean) -> val launchResourceIntent = Intent(Intent.ACTION_VIEW, Uri.parse(newsResource.url)) val context = LocalContext.current @@ -231,7 +249,7 @@ fun ForYouScreen( @Composable private fun TopicSelection( - uiState: FeedWithInterestsSelection, + interestsSelectionState: ForYouInterestsSelectionState.WithInterestsSelection, onTopicCheckedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier ) { @@ -253,7 +271,7 @@ private fun TopicSelection( .heightIn(max = max(240.dp, with(LocalDensity.current) { 240.sp.toDp() })) .fillMaxWidth() ) { - items(uiState.topics) { + items(interestsSelectionState.topics) { SingleTopicButton( name = it.topic.name, topicId = it.topic.id, @@ -314,7 +332,8 @@ fun ForYouScreenLoading() { MaterialTheme { Surface { ForYouScreen( - uiState = ForYouFeedUiState.Loading, + interestsSelectionState = ForYouInterestsSelectionState.Loading, + feedState = ForYouFeedState.Loading, onTopicCheckedChanged = { _, _ -> }, onAuthorCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -328,7 +347,7 @@ fun ForYouScreenLoading() { @Composable fun ForYouScreenTopicSelection() { ForYouScreen( - uiState = FeedWithInterestsSelection( + interestsSelectionState = ForYouInterestsSelectionState.WithInterestsSelection( topics = listOf( FollowableTopic( topic = Topic( @@ -364,86 +383,6 @@ fun ForYouScreenTopicSelection() { isFollowed = false ), ), - feed = listOf( - SaveableNewsResource( - newsResource = NewsResource( - id = "1", - episodeId = "52", - title = "Thanks for helping us reach 1M YouTube Subscribers", - content = "Thank you everyone for following the Now in Android series " + - "and everything the Android Developers YouTube channel has to offer. " + - "During the Android Developer Summit, our YouTube channel reached 1 " + - "million subscribers! Here’s a small video to thank you all.", - url = "https://youtu.be/-fJ6poHQrjM", - headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", - publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), - type = Video, - topics = listOf( - Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "", - url = "", - imageUrl = "" - ) - ), - authors = emptyList() - ), - isSaved = false - ), - SaveableNewsResource( - newsResource = NewsResource( - id = "2", - episodeId = "52", - title = "Transformations and customisations in the Paging Library", - content = "A demonstration of different operations that can be performed " + - "with Paging. Transformations like inserting separators, when to " + - "create a new pager, and customisation options for consuming " + - "PagingData.", - url = "https://youtu.be/ZARz0pjm5YM", - headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", - publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), - type = Video, - topics = listOf( - Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "", - url = "", - imageUrl = "" - ), - ), - authors = emptyList() - ), - isSaved = false - ), - SaveableNewsResource( - newsResource = NewsResource( - id = "3", - episodeId = "52", - title = "Community tip on Paging", - content = "Tips for using the Paging library from the developer community", - url = "https://youtu.be/r5JgIyS3t3s", - headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg", - publishDate = Instant.parse("2021-11-08T00:00:00.000Z"), - type = Video, - topics = listOf( - Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "", - url = "", - imageUrl = "" - ), - ), - authors = emptyList() - ), - isSaved = false - ), - ), authors = listOf( FollowableAuthor( author = Author( @@ -475,7 +414,10 @@ fun ForYouScreenTopicSelection() { ), isFollowed = false ) - ), + ) + ), + feedState = ForYouFeedState.Success( + feed = saveableNewsResource, ), onAuthorCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> }, @@ -490,8 +432,9 @@ fun PopulatedFeed() { MaterialTheme { Surface { ForYouScreen( - uiState = FeedWithoutTopicSelection( - feed = emptyList() + interestsSelectionState = ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = saveableNewsResource ), onTopicCheckedChanged = { _, _ -> }, onAuthorCheckedChanged = { _, _ -> }, @@ -501,3 +444,84 @@ fun PopulatedFeed() { } } } + +private val saveableNewsResource = listOf( + SaveableNewsResource( + newsResource = NewsResource( + id = "1", + episodeId = "52", + title = "Thanks for helping us reach 1M YouTube Subscribers", + content = "Thank you everyone for following the Now in Android series " + + "and everything the Android Developers YouTube channel has to offer. " + + "During the Android Developer Summit, our YouTube channel reached 1 " + + "million subscribers! Here’s a small video to thank you all.", + url = "https://youtu.be/-fJ6poHQrjM", + headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), + type = Video, + topics = listOf( + Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ) + ), + authors = emptyList() + ), + isSaved = false + ), + SaveableNewsResource( + newsResource = NewsResource( + id = "2", + episodeId = "52", + title = "Transformations and customisations in the Paging Library", + content = "A demonstration of different operations that can be performed " + + "with Paging. Transformations like inserting separators, when to " + + "create a new pager, and customisation options for consuming " + + "PagingData.", + url = "https://youtu.be/ZARz0pjm5YM", + headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), + type = Video, + topics = listOf( + Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ), + ), + authors = emptyList() + ), + isSaved = false + ), + SaveableNewsResource( + newsResource = NewsResource( + id = "3", + episodeId = "52", + title = "Community tip on Paging", + content = "Tips for using the Paging library from the developer community", + url = "https://youtu.be/r5JgIyS3t3s", + headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-08T00:00:00.000Z"), + type = Video, + topics = listOf( + Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "", + url = "", + imageUrl = "" + ), + ), + authors = emptyList() + ), + isSaved = false + ), +) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index f082a8cc8..e4f5d57b2 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable -import com.google.samples.apps.nowinandroid.core.domain.combine import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository @@ -32,14 +31,20 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsState.FollowedInterests +import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsState.None +import com.google.samples.apps.nowinandroid.feature.foryou.FollowedInterestsState.Unknown import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -52,24 +57,24 @@ class ForYouViewModel @Inject constructor( savedStateHandle: SavedStateHandle ) : ViewModel() { - private val followedInterestsStateFlow = + private val followedInterestsState: StateFlow = combine( authorsRepository.getFollowedAuthorIdsStream(), topicsRepository.getFollowedTopicIdsStream(), ) { followedAuthors, followedTopics -> if (followedAuthors.isEmpty() && followedTopics.isEmpty()) { - FollowedInterestsState.None + None } else { - FollowedInterestsState.FollowedInterests( + FollowedInterests( authorIds = followedAuthors, topicIds = followedTopics ) } } .stateIn( - viewModelScope, + scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = FollowedInterestsState.Unknown + initialValue = Unknown ) /** @@ -98,77 +103,92 @@ class ForYouViewModel @Inject constructor( mutableStateOf>(emptySet()) } - val uiState: StateFlow = combine( - followedInterestsStateFlow, - topicsRepository.getTopicsStream(), - snapshotFlow { inProgressTopicSelection }, - authorsRepository.getAuthorsStream(), - snapshotFlow { inProgressAuthorSelection }, - snapshotFlow { savedNewsResources } - ) { followedInterestsUserState, availableTopics, inProgressTopicSelection, - availableAuthors, inProgressAuthorSelection, savedNewsResources -> + val feedState: StateFlow = + combine( + followedInterestsState, + snapshotFlow { inProgressTopicSelection }, + snapshotFlow { inProgressAuthorSelection }, + snapshotFlow { savedNewsResources } + ) { followedInterestsUserState, inProgressTopicSelection, inProgressAuthorSelection, + savedNewsResources -> - fun mapToSaveableFeed(feed: List): List = - feed.map { newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = savedNewsResources.contains(newsResource.id) - ) + when (followedInterestsUserState) { + // If we don't know the current selection state, emit loading. + Unknown -> flowOf(ForYouFeedState.Loading) + // If the user has followed topics, use those followed topics to populate the feed + is FollowedInterests -> { + newsRepository.getNewsResourcesStream( + filterTopicIds = followedInterestsUserState.topicIds, + filterAuthorIds = followedInterestsUserState.authorIds + ).mapToFeedState(savedNewsResources) + } + // If the user hasn't followed interests yet, show a realtime populated feed based + // on the in-progress interests selections, if there are any. + None -> { + if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) { + flowOf(ForYouFeedState.Success(emptyList())) + } else { + newsRepository.getNewsResourcesStream( + filterTopicIds = inProgressTopicSelection, + filterAuthorIds = inProgressAuthorSelection + ).mapToFeedState(savedNewsResources) + } + } } + } + // Flatten the feed flows. + // As the selected topics and topic state changes, this will cancel the old feed + // monitoring and start the new one. + .flatMapLatest { it } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ForYouFeedState.Loading + ) - when (followedInterestsUserState) { - // If we don't know the current selection state, just emit loading. - FollowedInterestsState.Unknown -> flowOf(ForYouFeedUiState.Loading) - // If the user has followed topics, use those followed topics to populate the feed - is FollowedInterestsState.FollowedInterests -> { - newsRepository.getNewsResourcesStream( - filterTopicIds = followedInterestsUserState.topicIds, - filterAuthorIds = followedInterestsUserState.authorIds - ) - .map(::mapToSaveableFeed) - .map { feed -> - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = feed + val interestsSelectionState: StateFlow = + combine( + followedInterestsState, + topicsRepository.getTopicsStream(), + authorsRepository.getAuthorsStream(), + snapshotFlow { inProgressTopicSelection }, + snapshotFlow { inProgressAuthorSelection }, + ) { followedInterestsUserState, availableTopics, availableAuthors, inProgressTopicSelection, + inProgressAuthorSelection -> + + when (followedInterestsUserState) { + Unknown -> ForYouInterestsSelectionState.Loading + is FollowedInterests -> ForYouInterestsSelectionState.NoInterestsSelection + None -> { + val topics = availableTopics.map { topic -> + FollowableTopic( + topic = topic, + isFollowed = topic.id in inProgressTopicSelection ) } - } - // If the user hasn't followed topics yet, show the topic selection, as well as a - // realtime populated feed based on those in-progress topic selections. - FollowedInterestsState.None -> { - newsRepository.getNewsResourcesStream( - filterTopicIds = inProgressTopicSelection, - filterAuthorIds = inProgressAuthorSelection - ) - .map(::mapToSaveableFeed) - .map { feed -> - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = availableTopics.map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in inProgressTopicSelection - ) - }, - authors = availableAuthors.map { author -> - FollowableAuthor( - author = author, - isFollowed = author.id in inProgressAuthorSelection - ) - }, - feed = feed + val authors = availableAuthors.map { author -> + FollowableAuthor( + author = author, + isFollowed = author.id in inProgressAuthorSelection ) } + + if (topics.isEmpty() && authors.isEmpty()) { + ForYouInterestsSelectionState.Loading + } else { + ForYouInterestsSelectionState.WithInterestsSelection( + topics = topics, + authors = authors + ) + } + } } } - } - // Flatten the feed flows. - // As the selected topics and topic state changes, this will cancel the old feed monitoring - // and start the new one. - .flatMapLatest { it } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = ForYouFeedUiState.Loading - ) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ForYouInterestsSelectionState.Loading + ) fun updateTopicSelection(topicId: String, isChecked: Boolean) { withMutableSnapshot { @@ -223,67 +243,17 @@ class ForYouViewModel @Inject constructor( } } -/** - * A sealed hierarchy for the user's current followed interests state. - */ -private sealed interface FollowedInterestsState { - - /** - * The current state is unknown (hasn't loaded yet) - */ - object Unknown : FollowedInterestsState - - /** - * The user hasn't followed any interests yet. - */ - object None : FollowedInterestsState - - /** - * The user has followed the given (non-empty) set of [topicIds] or [authorIds]. - */ - data class FollowedInterests( - val topicIds: Set, - val authorIds: Set - ) : FollowedInterestsState -} - -/** - * A sealed hierarchy describing the for you screen state. - */ -sealed interface ForYouFeedUiState { - - /** - * The screen is still loading. - */ - object Loading : ForYouFeedUiState - - /** - * Loaded with a populated [feed] of [NewsResource]s. - */ - sealed interface PopulatedFeed : ForYouFeedUiState { - - /** - * The list of news resources contained in this [PopulatedFeed]. - */ - val feed: List - - /** - * The feed, along with a list of interests that can be selected. - */ - data class FeedWithInterestsSelection( - val topics: List, - val authors: List, - override val feed: List - ) : PopulatedFeed { - val canSaveInterests: Boolean = - topics.any { it.isFollowed } || authors.any { it.isFollowed } +private fun Flow>.mapToFeedState( + savedNewsResources: Set +): Flow = + filterNot { it.isEmpty() } + .map { newsResources -> + newsResources.map { newsResource -> + SaveableNewsResource( + newsResource = newsResource, + isSaved = savedNewsResources.contains(newsResource.id) + ) + } } - - /** - * Just the feed. - */ - data class FeedWithoutTopicSelection( - override val feed: List - ) : PopulatedFeed - } -} + .map, ForYouFeedState>(ForYouFeedState::Success) + .onStart { emit(ForYouFeedState.Loading) } diff --git a/feature-foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature-foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 97b1f8e8f..d27f78454 100644 --- a/feature-foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature-foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -29,6 +29,8 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsR import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import org.junit.Assert.assertEquals @@ -55,10 +57,32 @@ class ForYouViewModelTest { ) } + /** + * A pairing of [ForYouInterestsSelectionState] and [ForYouFeedState] for ease of testing + * state updates as a single flow. + */ + private data class ForYouUiState( + val interestsSelectionState: ForYouInterestsSelectionState, + val feedState: ForYouFeedState, + ) + + private val ForYouViewModel.uiState get() = + combine( + interestsSelectionState, + feedState, + ::ForYouUiState + ) + @Test fun stateIsInitiallyLoading() = runTest { viewModel.uiState.test { - assertEquals(ForYouFeedUiState.Loading, awaitItem()) + assertEquals( + ForYouUiState( + ForYouInterestsSelectionState.Loading, + ForYouFeedState.Loading + ), + awaitItem() + ) cancel() } } @@ -66,7 +90,13 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenFollowedTopicsAreLoading() = runTest { viewModel.uiState.test { - assertEquals(ForYouFeedUiState.Loading, awaitItem()) + assertEquals( + ForYouUiState( + ForYouInterestsSelectionState.Loading, + ForYouFeedState.Loading + ), + awaitItem() + ) topicsRepository.sendTopics(sampleTopics) cancel() @@ -76,7 +106,13 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenFollowedAuthorsAreLoading() = runTest { viewModel.uiState.test { - assertEquals(ForYouFeedUiState.Loading, awaitItem()) + assertEquals( + ForYouUiState( + ForYouInterestsSelectionState.Loading, + ForYouFeedState.Loading + ), + awaitItem() + ) authorsRepository.sendAuthors(sampleAuthors) cancel() @@ -86,7 +122,13 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenTopicsAreLoading() = runTest { viewModel.uiState.test { - assertEquals(ForYouFeedUiState.Loading, awaitItem()) + assertEquals( + ForYouUiState( + ForYouInterestsSelectionState.Loading, + ForYouFeedState.Loading + ), + awaitItem() + ) topicsRepository.setFollowedTopicIds(emptySet()) cancel() @@ -96,7 +138,13 @@ class ForYouViewModelTest { @Test fun stateIsLoadingWhenAuthorsAreLoading() = runTest { viewModel.uiState.test { - assertEquals(ForYouFeedUiState.Loading, awaitItem()) + assertEquals( + ForYouUiState( + ForYouInterestsSelectionState.Loading, + ForYouFeedState.Loading + ), + awaitItem() + ) authorsRepository.setFollowedAuthorIds(emptySet()) cancel() @@ -104,31 +152,20 @@ class ForYouViewModelTest { } @Test - fun stateIsLoadingWhenNewsResourcesAreLoading() = runTest { + fun stateIsInterestsSelectionWhenNewsResourcesAreLoading() = runTest { viewModel.uiState.test { - awaitItem() + advanceUntilIdle() + expectMostRecentItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) - cancel() - } - } - - @Test - fun stateIsTopicSelectionAfterLoadingEmptyFollowedTopicsAnAuthors() = runTest { - viewModel.uiState - .test { - awaitItem() - topicsRepository.sendTopics(sampleTopics) - authorsRepository.sendAuthors(sampleAuthors) - topicsRepository.setFollowedTopicIds(emptySet()) - authorsRepository.setFollowedAuthorIds(emptySet()) - newsRepository.sendNewsResources(sampleNewsResources) - - assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( + advanceUntilIdle() + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( topics = listOf( FollowableTopic( topic = Topic( @@ -196,33 +233,145 @@ class ForYouViewModelTest { isFollowed = false ) ), + ), + feedState = ForYouFeedState.Success( feed = emptyList() + ) + ), + expectMostRecentItem() + ) + + cancel() + } + } + + @Test + fun stateIsInterestsSelectionAfterLoadingEmptyFollowedTopicsAndAuthors() = runTest { + viewModel.uiState + .test { + topicsRepository.sendTopics(sampleTopics) + authorsRepository.sendAuthors(sampleAuthors) + topicsRepository.setFollowedTopicIds(emptySet()) + authorsRepository.setFollowedAuthorIds(emptySet()) + newsRepository.sendNewsResources(sampleNewsResources) + + advanceUntilIdle() + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + ), + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) + ), + ), + feedState = ForYouFeedState.Success( + feed = emptyList() + ) ), - awaitItem() + expectMostRecentItem() ) cancel() } } @Test - fun stateIsWithoutTopicSelectionAfterLoadingFollowedTopics() = runTest { + fun stateIsWithoutInterestsSelectionAfterLoadingFollowedTopics() = runTest { viewModel.uiState .test { - awaitItem() + advanceUntilIdle() + expectMostRecentItem() authorsRepository.sendAuthors(sampleAuthors) - authorsRepository.setFollowedAuthorIds(setOf("0", "1")) + authorsRepository.setFollowedAuthorIds(emptySet()) topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(setOf("0", "1")) + + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Loading + ), + awaitItem() + ) + newsRepository.sendNewsResources(sampleNewsResources) assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = sampleNewsResources.map { - SaveableNewsResource( - newsResource = it, - isSaved = false - ) - } + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = sampleNewsResources.map { + SaveableNewsResource( + newsResource = it, + isSaved = false + ) + } + ) ), awaitItem() ) @@ -231,24 +380,39 @@ class ForYouViewModelTest { } @Test - fun stateIsWithoutTopicSelectionAfterLoadingFollowedAuthors() = runTest { + fun stateIsWithoutInterestsSelectionAfterLoadingFollowedAuthors() = runTest { viewModel.uiState .test { - awaitItem() + advanceUntilIdle() + expectMostRecentItem() authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(setOf("0", "1")) topicsRepository.sendTopics(sampleTopics) - topicsRepository.setFollowedTopicIds(setOf("0", "1")) + topicsRepository.setFollowedTopicIds(emptySet()) + + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Loading + ), + awaitItem() + ) + newsRepository.sendNewsResources(sampleNewsResources) assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = sampleNewsResources.map { - SaveableNewsResource( - newsResource = it, - isSaved = false - ) - } + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = sampleNewsResources.map { + SaveableNewsResource( + newsResource = it, + isSaved = false + ) + } + ) ), awaitItem() ) @@ -260,93 +424,253 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterSelectingTopic() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() + advanceUntilIdle() + expectMostRecentItem() + viewModel.updateTopicSelection("1", isChecked = true) assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = true ), - isFollowed = true + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ) - ), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" + ), + feedState = ForYouFeedState.Success( + feed = emptyList(), + ) + ), + awaitItem() + ) + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = true + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ) + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) + ), ), - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = false + feedState = ForYouFeedState.Loading + ), + awaitItem() + ) + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = true + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) + ), + ), + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[1], + isSaved = false + ), + SaveableNewsResource( + newsResource = sampleNewsResources[2], + isSaved = false + ) ) ) ), @@ -360,93 +684,253 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterSelectingAuthor() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() + advanceUntilIdle() + expectMostRecentItem() + viewModel.updateAuthorSelection("1", isChecked = true) assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ) - ), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = true ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" + ), + feedState = ForYouFeedState.Success( + feed = emptyList(), + ) + ), + awaitItem() + ) + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = true + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ) + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = true + ), + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) + ), ), - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = false + feedState = ForYouFeedState.Loading + ), + awaitItem() + ) + assertEquals( + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) + ), + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ), + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = true + ), + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false + ), + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[1], + isSaved = false + ), + SaveableNewsResource( + newsResource = sampleNewsResources[2], + isSaved = false + ) ) ) ), @@ -460,91 +944,92 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterUnselectingTopic() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - - awaitItem() viewModel.updateTopicSelection("1", isChecked = true) - - awaitItem() viewModel.updateTopicSelection("1", isChecked = false) + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ), + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ) - ), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" - ), - isFollowed = false - ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" - ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" - ), - isFollowed = false - ) ), - feed = emptyList() + feedState = ForYouFeedState.Success( + feed = emptyList() + ) ), - awaitItem() + expectMostRecentItem() ) cancel() } @@ -554,91 +1039,92 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterUnselectingAuthor() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - - awaitItem() viewModel.updateAuthorSelection("1", isChecked = true) - - awaitItem() viewModel.updateAuthorSelection("1", isChecked = false) + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ) - ), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" - ), - isFollowed = false - ) ), - feed = emptyList() + feedState = ForYouFeedState.Success( + feed = emptyList() + ) ), - awaitItem() + expectMostRecentItem() ) cancel() } @@ -648,35 +1134,37 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterSavingTopicsOnly() = runTest { viewModel.uiState .test { - awaitItem() - topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() - viewModel.updateTopicSelection("1", isChecked = true) - awaitItem() + + advanceUntilIdle() + expectMostRecentItem() viewModel.saveFollowedInterests() - awaitItem() + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = false, - ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false, + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[1], + isSaved = false, + ), + SaveableNewsResource( + newsResource = sampleNewsResources[2], + isSaved = false, + ) ) ) ), - awaitItem() + expectMostRecentItem() ) assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics()) assertEquals(emptySet(), authorsRepository.getCurrentFollowedAuthors()) @@ -688,31 +1176,33 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterSavingAuthorsOnly() = runTest { viewModel.uiState .test { - awaitItem() - topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() - viewModel.updateAuthorSelection("0", isChecked = true) - awaitItem() + + advanceUntilIdle() + expectMostRecentItem() viewModel.saveFollowedInterests() - awaitItem() + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[0], - isSaved = false - ), + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[0], + isSaved = false + ), + ) ) ), - awaitItem() + expectMostRecentItem() ) assertEquals(emptySet(), topicsRepository.getCurrentFollowedTopics()) assertEquals(setOf("0"), authorsRepository.getCurrentFollowedAuthors()) @@ -724,36 +1214,38 @@ class ForYouViewModelTest { fun topicSelectionUpdatesAfterSavingAuthorsAndTopics() = runTest { viewModel.uiState .test { - awaitItem() - topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() - viewModel.updateAuthorSelection("1", isChecked = true) viewModel.updateTopicSelection("1", isChecked = true) - awaitItem() + + advanceUntilIdle() + expectMostRecentItem() viewModel.saveFollowedInterests() - awaitItem() + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = false - ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[1], + isSaved = false + ), + SaveableNewsResource( + newsResource = sampleNewsResources[2], + isSaved = false + ) ) ) ), - awaitItem() + expectMostRecentItem() ) assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics()) assertEquals(setOf("1"), authorsRepository.getCurrentFollowedAuthors()) @@ -765,91 +1257,98 @@ class ForYouViewModelTest { fun topicSelectionIsResetAfterSavingTopicsAndRemovingThem() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() - viewModel.updateTopicSelection("1", isChecked = true) viewModel.saveFollowedInterests() - awaitItem() + + advanceUntilIdle() + expectMostRecentItem() topicsRepository.setFollowedTopicIds(emptySet()) + + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ) - ), - feed = emptyList(), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" - ), - isFollowed = false - ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ) + ), + feedState = ForYouFeedState.Success( + feed = emptyList() ) ), - awaitItem() + + expectMostRecentItem() ) cancel() } @@ -859,91 +1358,97 @@ class ForYouViewModelTest { fun authorSelectionIsResetAfterSavingAuthorsAndRemovingThem() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(emptySet()) authorsRepository.sendAuthors(sampleAuthors) authorsRepository.setFollowedAuthorIds(emptySet()) newsRepository.sendNewsResources(sampleNewsResources) - awaitItem() - viewModel.updateAuthorSelection("1", isChecked = true) viewModel.saveFollowedInterests() - awaitItem() + + advanceUntilIdle() + expectMostRecentItem() authorsRepository.setFollowedAuthorIds(emptySet()) + + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithInterestsSelection( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.WithInterestsSelection( + topics = listOf( + FollowableTopic( + topic = Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false - ) - ), - feed = emptyList(), - authors = listOf( - FollowableAuthor( - author = Author( - id = "0", - name = "Android Dev", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableTopic( + topic = Topic( + id = "1", + name = "UI", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false ), - isFollowed = false + FollowableTopic( + topic = Topic( + id = "2", + name = "Tools", + shortDescription = "", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ), + isFollowed = false + ) ), - FollowableAuthor( - author = Author( - id = "1", - name = "Android Dev 2", - imageUrl = "", - twitter = "", - mediumPage = "" + authors = listOf( + FollowableAuthor( + author = Author( + id = "0", + name = "Android Dev", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false - ), - FollowableAuthor( - author = Author( - id = "2", - name = "Android Dev 3", - imageUrl = "", - twitter = "", - mediumPage = "" + FollowableAuthor( + author = Author( + id = "1", + name = "Android Dev 2", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false ), - isFollowed = false + FollowableAuthor( + author = Author( + id = "2", + name = "Android Dev 3", + imageUrl = "", + twitter = "", + mediumPage = "" + ), + isFollowed = false + ) ) + ), + feedState = ForYouFeedState.Success( + feed = emptyList() ) ), - awaitItem() + expectMostRecentItem() ) cancel() } @@ -953,7 +1458,6 @@ class ForYouViewModelTest { fun newsResourceSelectionUpdatesAfterLoadingFollowedTopics() = runTest { viewModel.uiState .test { - awaitItem() topicsRepository.sendTopics(sampleTopics) topicsRepository.setFollowedTopicIds(setOf("1")) authorsRepository.sendAuthors(sampleAuthors) @@ -961,20 +1465,25 @@ class ForYouViewModelTest { newsRepository.sendNewsResources(sampleNewsResources) viewModel.updateNewsResourceSaved("2", true) + advanceUntilIdle() assertEquals( - ForYouFeedUiState.PopulatedFeed.FeedWithoutTopicSelection( - feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = true - ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false + ForYouUiState( + interestsSelectionState = + ForYouInterestsSelectionState.NoInterestsSelection, + feedState = ForYouFeedState.Success( + feed = listOf( + SaveableNewsResource( + newsResource = sampleNewsResources[1], + isSaved = true + ), + SaveableNewsResource( + newsResource = sampleNewsResources[2], + isSaved = false + ) ) ) ), - awaitItem() + expectMostRecentItem() ) cancel() }