Merge "Enable bookmarks on authors page" into main

pull/205/head
TJ Dahunsi 2 years ago committed by Gerrit Code Review
commit 57cb02edc1

@ -24,6 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -52,10 +53,11 @@ class AuthorScreenTest {
fun niaLoadingWheel_whenScreenIsLoading_showLoading() { fun niaLoadingWheel_whenScreenIsLoading_showLoading() {
composeTestRule.setContent { composeTestRule.setContent {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Loading, authorUiState = AuthorUiState.Loading,
newsState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = { }, onBackClick = { },
onFollowClick = { } onFollowClick = { },
onBookmarkChanged = { _, _ -> },
) )
} }
@ -69,10 +71,11 @@ class AuthorScreenTest {
val testAuthor = testAuthors.first() val testAuthor = testAuthors.first()
composeTestRule.setContent { composeTestRule.setContent {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Success(testAuthor), authorUiState = AuthorUiState.Success(testAuthor),
newsState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = { }, onBackClick = { },
onFollowClick = { } onFollowClick = { },
onBookmarkChanged = { _, _ -> },
) )
} }
@ -91,10 +94,18 @@ class AuthorScreenTest {
fun news_whenAuthorIsLoading_isNotShown() { fun news_whenAuthorIsLoading_isNotShown() {
composeTestRule.setContent { composeTestRule.setContent {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Loading, authorUiState = AuthorUiState.Loading,
newsState = NewsUiState.Success(sampleNewsResources), newsUiState = NewsUiState.Success(
sampleNewsResources.mapIndexed { index, newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
),
onBackClick = { }, onBackClick = { },
onFollowClick = { } onFollowClick = { },
onBookmarkChanged = { _, _ -> },
) )
} }
@ -103,15 +114,24 @@ class AuthorScreenTest {
.onNodeWithContentDescription(authorLoading) .onNodeWithContentDescription(authorLoading)
.assertExists() .assertExists()
} }
@Test @Test
fun news_whenSuccessAndAuthorIsSuccess_isShown() { fun news_whenSuccessAndAuthorIsSuccess_isShown() {
val testAuthor = testAuthors.first() val testAuthor = testAuthors.first()
composeTestRule.setContent { composeTestRule.setContent {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Success(testAuthor), authorUiState = AuthorUiState.Success(testAuthor),
newsState = NewsUiState.Success(sampleNewsResources), newsUiState = NewsUiState.Success(
sampleNewsResources.mapIndexed { index, newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
),
onBackClick = { }, onBackClick = { },
onFollowClick = { } onFollowClick = { },
onBookmarkChanged = { _, _ -> },
) )
} }

@ -56,6 +56,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadi
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems
@ -67,24 +68,27 @@ fun AuthorRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: AuthorViewModel = hiltViewModel(), viewModel: AuthorViewModel = hiltViewModel(),
) { ) {
val uiState: AuthorScreenUiState by viewModel.uiState.collectAsStateWithLifecycle() val authorUiState: AuthorUiState by viewModel.authorUiState.collectAsStateWithLifecycle()
val newsUiState: NewsUiState by viewModel.newUiState.collectAsStateWithLifecycle()
AuthorScreen( AuthorScreen(
authorState = uiState.authorState, authorUiState = authorUiState,
newsState = uiState.newsState, newsUiState = newsUiState,
modifier = modifier, modifier = modifier,
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = viewModel::followAuthorToggle, onFollowClick = viewModel::followAuthorToggle,
onBookmarkChanged = viewModel::bookmarkNews,
) )
} }
@VisibleForTesting @VisibleForTesting
@Composable @Composable
internal fun AuthorScreen( internal fun AuthorScreen(
authorState: AuthorUiState, authorUiState: AuthorUiState,
newsState: NewsUiState, newsUiState: NewsUiState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onFollowClick: (Boolean) -> Unit, onFollowClick: (Boolean) -> Unit,
onBookmarkChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
@ -94,7 +98,7 @@ internal fun AuthorScreen(
item { item {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing))
} }
when (authorState) { when (authorUiState) {
AuthorUiState.Loading -> { AuthorUiState.Loading -> {
item { item {
NiaLoadingWheel( NiaLoadingWheel(
@ -111,12 +115,13 @@ internal fun AuthorScreen(
AuthorToolbar( AuthorToolbar(
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = onFollowClick, onFollowClick = onFollowClick,
uiState = authorState.followableAuthor, uiState = authorUiState.followableAuthor,
) )
} }
authorBody( authorBody(
author = authorState.followableAuthor.author, author = authorUiState.followableAuthor.author,
news = newsState news = newsUiState,
onBookmarkChanged = onBookmarkChanged,
) )
} }
} }
@ -128,13 +133,14 @@ internal fun AuthorScreen(
private fun LazyListScope.authorBody( private fun LazyListScope.authorBody(
author: Author, author: Author,
news: NewsUiState news: NewsUiState,
onBookmarkChanged: (String, Boolean) -> Unit
) { ) {
item { item {
AuthorHeader(author) AuthorHeader(author)
} }
authorCards(news) authorCards(news, onBookmarkChanged)
} }
@Composable @Composable
@ -163,14 +169,17 @@ private fun AuthorHeader(author: Author) {
} }
} }
private fun LazyListScope.authorCards(news: NewsUiState) { private fun LazyListScope.authorCards(
news: NewsUiState,
onBookmarkChanged: (String, Boolean) -> Unit
) {
when (news) { when (news) {
is NewsUiState.Success -> { is NewsUiState.Success -> {
newsResourceCardItems( newsResourceCardItems(
items = news.news, items = news.news,
newsResourceMapper = { it }, newsResourceMapper = { it.newsResource },
isBookmarkedMapper = { /* TODO */ false }, isBookmarkedMapper = { it.isSaved },
onToggleBookmark = { /* TODO */ }, onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) },
itemModifier = Modifier.padding(24.dp) itemModifier = Modifier.padding(24.dp)
) )
} }
@ -227,10 +236,18 @@ fun AuthorScreenPopulated() {
NiaTheme { NiaTheme {
NiaBackground { NiaBackground {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Success(FollowableAuthor(previewAuthors[0], false)), authorUiState = AuthorUiState.Success(FollowableAuthor(previewAuthors[0], false)),
newsState = NewsUiState.Success(previewNewsResources), newsUiState = NewsUiState.Success(
previewNewsResources.mapIndexed { index, newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
),
onBackClick = {}, onBackClick = {},
onFollowClick = {} onFollowClick = {},
onBookmarkChanged = { _, _ -> },
) )
} }
} }
@ -245,10 +262,11 @@ fun AuthorScreenLoading() {
NiaTheme { NiaTheme {
NiaBackground { NiaBackground {
AuthorScreen( AuthorScreen(
authorState = AuthorUiState.Loading, authorUiState = AuthorUiState.Loading,
newsState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = {}, onBackClick = {},
onFollowClick = {} onFollowClick = {},
onBookmarkChanged = { _, _ -> },
) )
} }
} }

@ -25,6 +25,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource 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.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination
@ -50,66 +51,126 @@ class AuthorViewModel @Inject constructor(
savedStateHandle[AuthorDestination.authorIdArg] savedStateHandle[AuthorDestination.authorIdArg]
) )
val authorUiState: StateFlow<AuthorUiState> = authorUiStateStream(
authorId = authorId,
userDataRepository = userDataRepository,
authorsRepository = authorsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = AuthorUiState.Loading
)
val newUiState: StateFlow<NewsUiState> = newsUiStateStream(
authorId = authorId,
userDataRepository = userDataRepository,
newsRepository = newsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading
)
fun followAuthorToggle(followed: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorId, followed)
}
}
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
}
}
private fun authorUiStateStream(
authorId: String,
userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
): Flow<AuthorUiState> {
// Observe the followed authors, as they could change over time. // Observe the followed authors, as they could change over time.
private val followedAuthorIdsStream: Flow<Result<Set<String>>> = val followedAuthorIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream userDataRepository.userDataStream
.map { it.followedAuthors } .map { it.followedAuthors }
.asResult()
// Observe author information // Observe author information
private val author: Flow<Result<Author>> = authorsRepository.getAuthorStream( val authorStream: Flow<Author> = authorsRepository.getAuthorStream(
id = authorId id = authorId
).asResult() )
// Observe the News for this author return combine(
private val newsStream: Flow<Result<List<NewsResource>>> = followedAuthorIdsStream,
newsRepository.getNewsResourcesStream( authorStream,
filterAuthorIds = setOf(element = authorId), ::Pair
filterTopicIds = emptySet() )
).asResult() .asResult()
.map { followedAuthorToAuthorResult ->
val uiState: StateFlow<AuthorScreenUiState> = when (followedAuthorToAuthorResult) {
combine( is Result.Success -> {
followedAuthorIdsStream, val (followedAuthors, author) = followedAuthorToAuthorResult.data
author, val followed = followedAuthors.contains(authorId)
newsStream
) { followedAuthorsResult, authorResult, newsResult ->
val author: AuthorUiState =
if (authorResult is Result.Success && followedAuthorsResult is Result.Success) {
val followed = followedAuthorsResult.data.contains(authorId)
AuthorUiState.Success( AuthorUiState.Success(
followableAuthor = FollowableAuthor( followableAuthor = FollowableAuthor(
author = authorResult.data, author = author,
isFollowed = followed isFollowed = followed
) )
) )
} else if ( }
authorResult is Result.Loading || followedAuthorsResult is Result.Loading is Result.Loading -> {
) {
AuthorUiState.Loading AuthorUiState.Loading
} else { }
is Result.Error -> {
AuthorUiState.Error AuthorUiState.Error
} }
val news: NewsUiState = when (newsResult) {
is Result.Success -> NewsUiState.Success(newsResult.data)
is Result.Loading -> NewsUiState.Loading
is Result.Error -> NewsUiState.Error
} }
AuthorScreenUiState(author, news)
} }
.stateIn( }
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = AuthorScreenUiState(AuthorUiState.Loading, NewsUiState.Loading)
)
fun followAuthorToggle(followed: Boolean) { private fun newsUiStateStream(
viewModelScope.launch { authorId: String,
userDataRepository.toggleFollowedAuthorId(authorId, followed) newsRepository: NewsRepository,
userDataRepository: UserDataRepository,
): Flow<NewsUiState> {
// Observe news
val newsStream: Flow<List<NewsResource>> = newsRepository.getNewsResourcesStream(
filterAuthorIds = setOf(element = authorId),
filterTopicIds = emptySet()
)
// Observe bookmarks
val bookmarkStream: Flow<Set<String>> = userDataRepository.userDataStream
.map { it.bookmarkedNewsResources }
return combine(
newsStream,
bookmarkStream,
::Pair
)
.asResult()
.map { newsToBookmarksResult ->
when (newsToBookmarksResult) {
is Result.Success -> {
val (news, bookmarks) = newsToBookmarksResult.data
NewsUiState.Success(
news.map { newsResource ->
SaveableNewsResource(
newsResource,
isSaved = bookmarks.contains(newsResource.id)
)
}
)
}
is Result.Loading -> {
NewsUiState.Loading
}
is Result.Error -> {
NewsUiState.Error
}
}
} }
}
} }
sealed interface AuthorUiState { sealed interface AuthorUiState {
@ -119,12 +180,7 @@ sealed interface AuthorUiState {
} }
sealed interface NewsUiState { sealed interface NewsUiState {
data class Success(val news: List<NewsResource>) : NewsUiState data class Success(val news: List<SaveableNewsResource>) : NewsUiState
object Error : NewsUiState object Error : NewsUiState
object Loading : NewsUiState object Loading : NewsUiState
} }
data class AuthorScreenUiState(
val authorState: AuthorUiState,
val newsState: NewsUiState
)

@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -68,16 +69,16 @@ class AuthorViewModelTest {
@Test @Test
fun uiStateAuthor_whenSuccess_matchesAuthorFromRepository() = runTest { fun uiStateAuthor_whenSuccess_matchesAuthorFromRepository() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() } val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.authorUiState.collect() }
// To make sure AuthorUiState is success // To make sure AuthorUiState is success
authorsRepository.sendAuthors(testInputAuthors.map(FollowableAuthor::author)) authorsRepository.sendAuthors(testInputAuthors.map(FollowableAuthor::author))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = viewModel.uiState.value val item = viewModel.authorUiState.value
assertTrue(item.authorState is AuthorUiState.Success) assertTrue(item is AuthorUiState.Success)
val successAuthorUiState = item.authorState as AuthorUiState.Success val successAuthorUiState = item as AuthorUiState.Success
val authorFromRepository = authorsRepository.getAuthorStream( val authorFromRepository = authorsRepository.getAuthorStream(
id = testInputAuthors[0].author.id id = testInputAuthors[0].author.id
).first() ).first()
@ -90,20 +91,20 @@ class AuthorViewModelTest {
@Test @Test
fun uiStateNews_whenInitialized_thenShowLoading() = runTest { fun uiStateNews_whenInitialized_thenShowLoading() = runTest {
assertEquals(NewsUiState.Loading, viewModel.uiState.value.newsState) assertEquals(NewsUiState.Loading, viewModel.newUiState.value)
} }
@Test @Test
fun uiStateAuthor_whenInitialized_thenShowLoading() = runTest { fun uiStateAuthor_whenInitialized_thenShowLoading() = runTest {
assertEquals(AuthorUiState.Loading, viewModel.uiState.value.authorState) assertEquals(AuthorUiState.Loading, viewModel.authorUiState.value)
} }
@Test @Test
fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorLoading_thenShowLoading() = runTest { fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorLoading_thenShowLoading() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() } val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.authorUiState.collect() }
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
assertEquals(AuthorUiState.Loading, viewModel.uiState.value.authorState) assertEquals(AuthorUiState.Loading, viewModel.authorUiState.value)
collectJob.cancel() collectJob.cancel()
} }
@ -111,13 +112,21 @@ class AuthorViewModelTest {
@Test @Test
fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorSuccess_thenAuthorSuccessAndNewsLoading() = fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorSuccess_thenAuthorSuccessAndNewsLoading() =
runTest { runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() } val collectJob = launch(UnconfinedTestDispatcher()) {
combine(
viewModel.authorUiState,
viewModel.newUiState,
::Pair
).collect()
}
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = viewModel.uiState.value val authorState = viewModel.authorUiState.value
assertTrue(item.authorState is AuthorUiState.Success) val newsUiState = viewModel.newUiState.value
assertTrue(item.newsState is NewsUiState.Loading)
assertTrue(authorState is AuthorUiState.Success)
assertTrue(newsUiState is NewsUiState.Loading)
collectJob.cancel() collectJob.cancel()
} }
@ -125,21 +134,29 @@ class AuthorViewModelTest {
@Test @Test
fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorSuccessAndNewsIsSuccess_thenAllSuccess() = fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorSuccessAndNewsIsSuccess_thenAllSuccess() =
runTest { runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() } val collectJob = launch(UnconfinedTestDispatcher()) {
combine(
viewModel.authorUiState,
viewModel.newUiState,
::Pair
).collect()
}
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
val item = viewModel.uiState.value val authorState = viewModel.authorUiState.value
assertTrue(item.authorState is AuthorUiState.Success) val newsUiState = viewModel.newUiState.value
assertTrue(item.newsState is NewsUiState.Success)
assertTrue(authorState is AuthorUiState.Success)
assertTrue(newsUiState is NewsUiState.Success)
collectJob.cancel() collectJob.cancel()
} }
@Test @Test
fun uiStateAuthor_whenFollowingAuthor_thenShowUpdatedAuthor() = runTest { fun uiStateAuthor_whenFollowingAuthor_thenShowUpdatedAuthor() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() } val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.authorUiState.collect() }
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
// Set which author IDs are followed, not including 0. // Set which author IDs are followed, not including 0.
@ -149,7 +166,35 @@ class AuthorViewModelTest {
assertEquals( assertEquals(
AuthorUiState.Success(followableAuthor = testOutputAuthors[0]), AuthorUiState.Success(followableAuthor = testOutputAuthors[0]),
viewModel.uiState.value.authorState viewModel.authorUiState.value
)
collectJob.cancel()
}
@Test
fun uiStateAuthor_whenNewsBookmarked_thenShowBookmarkedNews() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.newUiState.collect() }
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
newsRepository.sendNewsResources(sampleNewsResources)
// Set initial bookmarked status to false
userDataRepository.updateNewsResourceBookmark(
newsResourceId = sampleNewsResources.first().id,
bookmarked = false
)
viewModel.bookmarkNews(
newsResourceId = sampleNewsResources.first().id,
bookmarked = true
)
assertTrue(
(viewModel.newUiState.value as NewsUiState.Success)
.news
.first { it.newsResource.id == sampleNewsResources.first().id }
.isSaved
) )
collectJob.cancel() collectJob.cancel()

Loading…
Cancel
Save