Merge pull request #521 from dturner/chipcolor

Move construction of UserNewsResource into separate function
pull/508/head
Don Turner 2 years ago committed by GitHub
commit d3cccdc66f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,8 +18,8 @@ package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
@ -48,30 +48,13 @@ class GetUserNewsResourcesUseCase @Inject constructor(
newsRepository.getNewsResources()
} else {
newsRepository.getNewsResources(filterTopicIds = filterTopicIds)
}.mapToSaveableNewsResources(userDataRepository.userData)
}.mapToUserNewsResources(userDataRepository.userData)
}
private fun Flow<List<NewsResource>>.mapToSaveableNewsResources(
userData: Flow<UserData>
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>
): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() }
.combine(userData) { newsResources, userData ->
newsResources.map { newsResource ->
UserNewsResource(
id = newsResource.id,
title = newsResource.title,
content = newsResource.content,
url = newsResource.url,
headerImageUrl = newsResource.headerImageUrl,
publishDate = newsResource.publishDate,
type = newsResource.type,
topics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id)
)
}
.combine(userDataStream) { newsResources, userData ->
newsResources.mapToUserNewsResources(userData)
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Unknown
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
@ -29,9 +30,10 @@ import kotlinx.datetime.toInstant
/* ktlint-disable max-line-length */
/**
* A [NewsResource] with the additional user information.
* A [NewsResource] with additional user information such as whether the user is following the
* news resource's topics and whether they have saved (bookmarked) this news resource.
*/
data class UserNewsResource(
data class UserNewsResource internal constructor(
val id: String,
val title: String,
val content: String,
@ -39,9 +41,30 @@ data class UserNewsResource(
val headerImageUrl: String?,
val publishDate: Instant,
val type: NewsResourceType,
val topics: List<FollowableTopic>,
val followableTopics: List<FollowableTopic>,
val isSaved: Boolean
)
) {
constructor(newsResource: NewsResource, userData: UserData) : this(
id = newsResource.id,
title = newsResource.title,
content = newsResource.content,
url = newsResource.url,
headerImageUrl = newsResource.headerImageUrl,
publishDate = newsResource.publishDate,
type = newsResource.type,
followableTopics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id)
)
}
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> {
return map { UserNewsResource(it, userData) }
}
val previewUserNewsResources = listOf(
UserNewsResource(
@ -60,7 +83,7 @@ val previewUserNewsResources = listOf(
nanosecond = 0
).toInstant(TimeZone.UTC),
type = Codelab,
topics = listOf(previewFollowableTopics[1]),
followableTopics = listOf(previewFollowableTopics[1]),
isSaved = true
),
UserNewsResource(
@ -74,7 +97,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
isSaved = false
),
UserNewsResource(
@ -88,7 +111,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
topics = listOf(previewFollowableTopics[2]),
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = false
),
UserNewsResource(
@ -100,7 +123,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown,
topics = listOf(previewFollowableTopics[2]),
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = true
)
)

@ -16,11 +16,13 @@
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
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.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
@ -44,107 +46,59 @@ class GetUserNewsResourcesUseCaseTest {
@Test
fun whenNoFilters_allNewsResourcesAreReturned() = runTest {
// Obtain the saveable news resources stream.
val saveableNewsResources = useCase()
// Obtain the user news resources stream.
val userNewsResources = useCase()
// Send some news resources and bookmarks.
// Send some news resources and user data into the data repositories.
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(
setOf(sampleNewsResources[0].id, sampleNewsResources[2].id)
// Construct the test user data with bookmarks and followed topics.
val userData = UserData(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = false
)
// Set a followed topic for the user.
userDataRepository.setFollowedTopicIds(setOf(sampleTopic1.id))
userDataRepository.setUserData(userData)
// Check that the correct news resources are returned with their bookmarked state.
assertEquals(
listOf(
UserNewsResource(
sampleNewsResources[0].id,
sampleNewsResources[0].title,
sampleNewsResources[0].content,
sampleNewsResources[0].url,
sampleNewsResources[0].headerImageUrl,
sampleNewsResources[0].publishDate,
sampleNewsResources[0].type,
sampleNewsResources[0].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
true
),
UserNewsResource(
sampleNewsResources[1].id,
sampleNewsResources[1].title,
sampleNewsResources[1].content,
sampleNewsResources[1].url,
sampleNewsResources[1].headerImageUrl,
sampleNewsResources[1].publishDate,
sampleNewsResources[1].type,
sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
false
),
UserNewsResource(
sampleNewsResources[2].id,
sampleNewsResources[2].title,
sampleNewsResources[2].content,
sampleNewsResources[2].url,
sampleNewsResources[2].headerImageUrl,
sampleNewsResources[2].publishDate,
sampleNewsResources[2].type,
sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
true
),
UserNewsResource(sampleNewsResources[0], userData),
UserNewsResource(sampleNewsResources[1], userData),
UserNewsResource(sampleNewsResources[2], userData),
),
saveableNewsResources.first()
userNewsResources.first()
)
}
@Test
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of saveable news resources for the given topic id.
val saveableNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
// Obtain a stream of user news resources for the given topic id.
val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
// Send some news resources and bookmarks.
// Send test data into the repositories.
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(setOf())
val userData = UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = false
)
userDataRepository.setUserData(userData)
// Check that only news resources with the given topic id are returned.
assertEquals(
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.map {
UserNewsResource(
id = it.id,
title = it.title,
content = it.content,
url = it.url,
headerImageUrl = it.headerImageUrl,
publishDate = it.publishDate,
type = it.type,
topics = it.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = false
)
},
isSaved = false
)
UserNewsResource(it, userData)
},
saveableNewsResources.first()
userNewsResources.first()
)
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2023 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.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class UserNewsResourceTest {
/**
* Given: Some user data and news resources
* When: They are combined using `UserNewsResource.from`
* Then: The correct UserNewsResources are constructed
*/
@Test
fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() {
val newsResource1 = NewsResource(
id = "N1",
title = "Test news title",
content = "Test news content",
url = "Test URL",
headerImageUrl = "Test image URL",
publishDate = Clock.System.now(),
type = Article,
topics = listOf(
Topic(
id = "T1",
name = "Topic 1",
shortDescription = "Topic 1 short description",
longDescription = "Topic 1 long description",
url = "Topic 1 URL",
imageUrl = "Topic 1 image URL"
),
Topic(
id = "T2",
name = "Topic 2",
shortDescription = "Topic 2 short description",
longDescription = "Topic 2 long description",
url = "Topic 2 URL",
imageUrl = "Topic 2 image URL"
),
)
)
val userData = UserData(
bookmarkedNewsResources = setOf("N1"),
followedTopics = setOf("T1"),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = true
)
val userNewsResource = UserNewsResource(newsResource1, userData)
// Check that the simple field mappings have been done correctly.
assertEquals(newsResource1.id, userNewsResource.id)
assertEquals(newsResource1.title, userNewsResource.title)
assertEquals(newsResource1.content, userNewsResource.content)
assertEquals(newsResource1.url, userNewsResource.url)
assertEquals(newsResource1.headerImageUrl, userNewsResource.headerImageUrl)
assertEquals(newsResource1.publishDate, userNewsResource.publishDate)
// Check that each Topic has been converted to a FollowedTopic correctly.
assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size)
for (topic in newsResource1.topics) {
// Construct the expected FollowableTopic.
val followableTopic = FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
assertTrue(userNewsResource.followableTopics.contains(followableTopic))
}
// Check that the saved flag is set correctly.
assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id),
userNewsResource.isSaved
)
}
}

@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
private val emptyUserData = UserData(
val emptyUserData = UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
@ -98,4 +98,11 @@ class TestUserDataRepository : UserDataRepository {
*/
fun getCurrentFollowedTopics(): Set<String>? =
_userData.replayCache.firstOrNull()?.followedTopics
/**
* A test-only API to allow setting of user data directly.
*/
fun setUserData(userData: UserData) {
_userData.tryEmit(userData)
}
}

@ -79,7 +79,7 @@ class NewsResourceCardTest {
@Test
fun testTopicsChipColorBackground_matchesFollowedState() {
val followableTopics = previewUserNewsResources[1].topics
val followableTopics = previewUserNewsResources[1].followableTopics
composeTestRule.setContent {
NewsResourceTopics(topics = followableTopics)

@ -116,7 +116,7 @@ fun NewsResourceCardExpanded(
Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(userNewsResource.content)
Spacer(modifier = Modifier.height(12.dp))
NewsResourceTopics(userNewsResource.topics)
NewsResourceTopics(userNewsResource.followableTopics)
}
}
}

@ -20,12 +20,14 @@ import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCa
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
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.Topic
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.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor
@ -265,7 +267,8 @@ class ForYouViewModelTest {
topicsRepository.sendTopics(sampleTopics)
val followedTopicIds = setOf("0", "1")
userDataRepository.setFollowedTopicIds(followedTopicIds)
val userData = emptyUserData.copy(followedTopics = followedTopicIds)
userDataRepository.setUserData(userData)
viewModel.dismissOnboarding()
assertEquals(
@ -282,25 +285,7 @@ class ForYouViewModelTest {
)
assertEquals(
NewsFeedUiState.Success(
feed =
sampleNewsResources.map {
UserNewsResource(
id = it.id,
title = it.title,
content = it.content,
url = it.url,
headerImageUrl = it.headerImageUrl,
publishDate = it.publishDate,
type = it.type,
topics = it.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)
)
},
isSaved = false
)
}
feed = sampleNewsResources.mapToUserNewsResources(userData)
),
viewModel.feedState.value
)
@ -345,41 +330,14 @@ class ForYouViewModelTest {
),
viewModel.onboardingUiState.value
)
val userData = emptyUserData.copy(followedTopics = setOf(followedTopicId))
assertEquals(
NewsFeedUiState.Success(
feed = listOf(
UserNewsResource(
id = sampleNewsResources[1].id,
title = sampleNewsResources[1].title,
content = sampleNewsResources[1].content,
url = sampleNewsResources[1].url,
headerImageUrl = sampleNewsResources[1].headerImageUrl,
publishDate = sampleNewsResources[1].publishDate,
type = sampleNewsResources[1].type,
topics = sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == followedTopicId
)
},
isSaved = false
),
UserNewsResource(
id = sampleNewsResources[2].id,
title = sampleNewsResources[2].title,
content = sampleNewsResources[2].content,
url = sampleNewsResources[2].url,
headerImageUrl = sampleNewsResources[2].headerImageUrl,
publishDate = sampleNewsResources[2].publishDate,
type = sampleNewsResources[2].type,
topics = sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == followedTopicId
)
},
isSaved = false
)
UserNewsResource(sampleNewsResources[1], userData),
UserNewsResource(sampleNewsResources[2], userData),
)
),
viewModel.feedState.value
@ -460,12 +418,24 @@ class ForYouViewModelTest {
val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() }
val followedTopicIds = setOf("1")
val userData = emptyUserData.copy(
followedTopics = followedTopicIds,
shouldHideOnboarding = true
)
topicsRepository.sendTopics(sampleTopics)
userDataRepository.setFollowedTopicIds(followedTopicIds)
userDataRepository.setShouldHideOnboarding(true)
userDataRepository.setUserData(userData)
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved("2", true)
val bookmarkedNewsResourceId = "2"
viewModel.updateNewsResourceSaved(
newsResourceId = bookmarkedNewsResourceId,
isChecked = true
)
val userDataExpected = userData.copy(
bookmarkedNewsResources = setOf(bookmarkedNewsResourceId)
)
assertEquals(
OnboardingUiState.NotShown,
@ -474,38 +444,8 @@ class ForYouViewModelTest {
assertEquals(
NewsFeedUiState.Success(
feed = listOf(
UserNewsResource(
id = sampleNewsResources[1].id,
title = sampleNewsResources[1].title,
content = sampleNewsResources[1].content,
url = sampleNewsResources[1].url,
headerImageUrl = sampleNewsResources[1].headerImageUrl,
publishDate = sampleNewsResources[1].publishDate,
type = sampleNewsResources[1].type,
topics = sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)
)
},
isSaved = true
),
UserNewsResource(
id = sampleNewsResources[2].id,
title = sampleNewsResources[2].title,
content = sampleNewsResources[2].content,
url = sampleNewsResources[2].url,
headerImageUrl = sampleNewsResources[2].headerImageUrl,
publishDate = sampleNewsResources[2].publishDate,
type = sampleNewsResources[2].type,
topics = sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)
)
},
isSaved = false
),
UserNewsResource(newsResource = sampleNewsResources[1], userDataExpected),
UserNewsResource(newsResource = sampleNewsResources[2], userDataExpected)
)
),
viewModel.feedState.value

@ -187,7 +187,7 @@ private val sampleUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(
followableTopics = listOf(
FollowableTopic(
topic = Topic(
id = "0",

Loading…
Cancel
Save