test(home): 팔로잉 최근 소식 nested payload 검증을 추가한다

This commit is contained in:
2026-06-30 23:53:58 +09:00
parent 81a2b541a0
commit 2041036530
2 changed files with 218 additions and 21 deletions

View File

@@ -153,7 +153,7 @@ class HomeFollowingFragmentSourceTest {
)
assertFalse(fragment.contains("binding.viewHomeFollowingRecentNewsTitle.ivSectionTitleChevron"))
assertTrue(recentNewsSection.contains("view_home_following_recent_news_title"))
assertTrue(adapter.contains("FollowingNewsType.COMMUNITY_POST -> VIEW_TYPE_COMMUNITY"))
assertTrue(adapter.contains("is HomeFollowingNewsUiItem.Community -> VIEW_TYPE_COMMUNITY"))
assertTrue(adapter.contains("is HomeFollowingNewsUiItem.Ranking -> VIEW_TYPE_RANKING"))
assertTrue(adapter.contains("R.layout.view_feed_community"))
assertTrue(adapter.contains("R.layout.view_feed_rank"))
@@ -174,7 +174,7 @@ class HomeFollowingFragmentSourceTest {
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
).readText()
assertTrue(adapter.contains("ranking.creatorNickname"))
assertTrue(adapter.contains("creatorNickname"))
assertTrue(adapter.contains("ranking.rank"))
assertTrue(adapter.contains("R.string.screen_home_following_creator_ranking_news_message"))
assertTrue(adapter.contains("context.getString("))
@@ -186,6 +186,59 @@ class HomeFollowingFragmentSourceTest {
assertFalse(adapter.contains("highlightRanges = emptyList()"))
}
@Test
fun `following recent news models use nested payload response contract`() {
val models = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt"
).readText()
val newsResponse = models.substringAfter("data class FollowingNewsResponse(")
.substringBefore(")\n\n@Keep")
assertTrue(newsResponse.contains("newsId"))
assertTrue(newsResponse.contains("type"))
assertTrue(newsResponse.contains("visibleFromAtUtc"))
assertTrue(newsResponse.contains("creatorRanking"))
assertTrue(newsResponse.contains("audioContent"))
assertTrue(newsResponse.contains("photoContent"))
assertTrue(newsResponse.contains("contentRanking"))
assertTrue(newsResponse.contains("communityPost"))
assertFalse(newsResponse.contains("creatorProfileImageUrl"))
assertFalse(newsResponse.contains("creatorNickname"))
assertFalse(newsResponse.contains("title"))
assertFalse(newsResponse.contains("body"))
assertFalse(newsResponse.contains("thumbnailImageUrl"))
assertFalse(newsResponse.contains("targetId"))
assertFalse(newsResponse.contains("occurredAtUtc"))
assertFalse(newsResponse.contains("rank"))
assertTrue(models.contains("data class FollowingCreatorRankingNewsResponse"))
assertTrue(models.contains("data class FollowingContentNewsResponse"))
assertTrue(models.contains("data class FollowingContentRankingNewsResponse"))
assertTrue(models.contains("data class FollowingCommunityPostNewsResponse"))
}
@Test
fun `following recent news adapter binds nested payload ui variants`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
).readText()
assertTrue(adapter.contains("FollowingNewsType.CONTENT_RANKING"))
assertTrue(adapter.contains("R.string.screen_home_following_content_ranking_news_message"))
assertTrue(adapter.contains("ranking.imageUrl"))
assertTrue(adapter.contains("community.postId.toString()"))
assertTrue(adapter.contains("community.likeCount"))
assertTrue(adapter.contains("community.commentCount"))
assertTrue(adapter.contains("community.content"))
assertTrue(adapter.contains("community.imageUrl"))
assertTrue(adapter.contains("content.contentId.toString()"))
assertTrue(adapter.contains("content.contentImageUrl"))
assertTrue(adapter.contains("content.creatorProfileImageUrl"))
assertFalse(adapter.contains("commentCount = 0"))
assertFalse(adapter.contains("likeCount = 0"))
assertFalse(adapter.contains("community.body.ifBlank { community.title }"))
assertFalse(adapter.contains("ranking.thumbnailImageUrl.orEmpty()"))
}
@Test
fun `following creators section has no header and uses figma simple profile size`() {
val layout = homeMainLayoutSource()

View File

@@ -4,7 +4,11 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCommunityPostNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingContentNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingContentRankingNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorRankingNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingLiveResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
@@ -14,6 +18,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingNewsUiItem
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingUiState
import kr.co.vividnext.sodalive.v2.main.home.model.toUiState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -42,7 +47,7 @@ class HomeFollowingMapperTest {
onAirLives = listOf(live()),
recentChats = listOf(chat()),
monthlySchedules = listOf(schedule(scheduleId = "schedule-1")),
recentNews = listOf(news(newsId = "news-1", type = FollowingNewsType.PHOTO_CONTENT))
recentNews = listOf(photoContentNews(newsId = "news-1"))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(1L, state.followingCreators.items.single().creatorId)
@@ -73,7 +78,7 @@ class HomeFollowingMapperTest {
@Test
fun `PHOTO_CONTENT는 photo content label로 매핑된다`() {
val state = response(
recentNews = listOf(news(type = FollowingNewsType.PHOTO_CONTENT))
recentNews = listOf(photoContentNews())
).toUiState(formatter) as HomeFollowingUiState.Content
val item = state.recentNews.items.single() as HomeFollowingNewsUiItem.Content
@@ -81,21 +86,96 @@ class HomeFollowingMapperTest {
}
@Test
fun `ranking news의 rank null 항목은 제외된다`() {
fun `recentNews nested payload는 type별 UI item으로 매핑된다`() {
val state = response(
recentNews = listOf(
news(newsId = "hidden", type = FollowingNewsType.CREATOR_RANKING, rank = null),
news(newsId = "shown", type = FollowingNewsType.CONTENT_RANKING, rank = 1)
creatorRankingNews(newsId = "creator-ranking"),
contentRankingNews(newsId = "content-ranking"),
audioContentNews(newsId = "audio-content"),
photoContentNews(newsId = "photo-content"),
communityPostNews(newsId = "community-post")
)
).toUiState(formatter) as HomeFollowingUiState.Content
val creatorRanking = state.recentNews.items[0] as HomeFollowingNewsUiItem.Ranking
assertEquals(FollowingNewsType.CREATOR_RANKING, creatorRanking.type)
assertEquals(7, creatorRanking.rank)
assertEquals(1001L, creatorRanking.targetId)
assertEquals("creator nickname", creatorRanking.creatorNickname)
assertEquals("https://example.com/creator-ranking.png", creatorRanking.imageUrl)
val contentRanking = state.recentNews.items[1] as HomeFollowingNewsUiItem.Ranking
assertEquals(FollowingNewsType.CONTENT_RANKING, contentRanking.type)
assertEquals(3, contentRanking.rank)
assertEquals(2001L, contentRanking.targetId)
assertEquals("ranked content", contentRanking.title)
assertEquals("https://example.com/content-ranking.png", contentRanking.imageUrl)
val audioContent = state.recentNews.items[2] as HomeFollowingNewsUiItem.Content
assertEquals(FollowingNewsType.AUDIO_CONTENT, audioContent.type)
assertEquals(3001L, audioContent.contentId)
assertEquals("audio title", audioContent.title)
assertEquals("https://example.com/audio.png", audioContent.contentImageUrl)
assertEquals("audio creator", audioContent.creatorNickname)
val photoContent = state.recentNews.items[3] as HomeFollowingNewsUiItem.Content
assertEquals(FollowingNewsType.PHOTO_CONTENT, photoContent.type)
assertEquals(3002L, photoContent.contentId)
assertEquals(R.string.screen_home_following_photo_content, photoContent.labelResId)
val community = state.recentNews.items[4] as HomeFollowingNewsUiItem.Community
assertEquals(4001L, community.postId)
assertEquals("community creator", community.creatorNickname)
assertEquals("https://example.com/community-image.png", community.imageUrl)
assertEquals("community body", community.content)
assertEquals("2026-06-25T02:00:00Z", community.createdAt)
assertEquals(11, community.likeCount)
assertEquals(5, community.commentCount)
}
@Test
fun `type에 대응하는 nested payload가 null이면 recentNews에서 제외된다`() {
val state = response(
recentNews = listOf(
FollowingNewsResponse(
newsId = "missing-creator-ranking",
type = FollowingNewsType.CREATOR_RANKING,
visibleFromAtUtc = "2026-06-25T01:00:00Z",
creatorRanking = null
),
FollowingNewsResponse(
newsId = "missing-audio-content",
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = "2026-06-25T01:00:00Z",
audioContent = null
),
contentRankingNews(newsId = "shown")
)
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(listOf("shown"), state.recentNews.items.map { it.newsId })
}
@Test
fun `contentImageUrl이 null이어도 최근 소식 content와 ranking item은 유지된다`() {
val state = response(
recentNews = listOf(
contentRankingNews(newsId = "content-ranking", contentImageUrl = null),
audioContentNews(newsId = "audio-content", contentImageUrl = null)
)
).toUiState(formatter) as HomeFollowingUiState.Content
val ranking = state.recentNews.items[0] as HomeFollowingNewsUiItem.Ranking
assertNull(ranking.imageUrl)
val content = state.recentNews.items[1] as HomeFollowingNewsUiItem.Content
assertNull(content.contentImageUrl)
}
@Test
fun `news 상대 시간은 visibleFromAtUtc 값을 formatter에 전달한다`() {
val state = response(
recentNews = listOf(news(visibleFromAtUtc = "2026-06-25T00:00:00Z"))
recentNews = listOf(audioContentNews(visibleFromAtUtc = "2026-06-25T00:00:00Z"))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals("relative:2026-06-25T00:00:00Z", state.recentNews.items.single().visibleFromText)
@@ -152,22 +232,86 @@ class HomeFollowingMapperTest {
isOnAir = false
)
private fun news(
private fun creatorRankingNews(
newsId: String = "news",
type: FollowingNewsType = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc: String = "2026-06-25T01:00:00Z",
rank: Int? = null
visibleFromAtUtc: String = "2026-06-25T01:00:00Z"
) = FollowingNewsResponse(
newsId = newsId,
type = type,
creatorProfileImageUrl = "https://example.com/news.png",
creatorNickname = "creator",
title = "title",
body = "body",
thumbnailImageUrl = null,
targetId = 6L,
occurredAtUtc = "2026-06-25T00:00:00Z",
type = FollowingNewsType.CREATOR_RANKING,
visibleFromAtUtc = visibleFromAtUtc,
rank = rank
creatorRanking = FollowingCreatorRankingNewsResponse(
rank = 7,
creatorId = 1001L,
nickname = "creator nickname",
profileImageUrl = "https://example.com/creator-ranking.png"
)
)
private fun contentRankingNews(
newsId: String = "news",
visibleFromAtUtc: String = "2026-06-25T01:00:00Z",
contentImageUrl: String? = "https://example.com/content-ranking.png"
) = FollowingNewsResponse(
newsId = newsId,
type = FollowingNewsType.CONTENT_RANKING,
visibleFromAtUtc = visibleFromAtUtc,
contentRanking = FollowingContentRankingNewsResponse(
rank = 3,
contentId = 2001L,
contentImageUrl = contentImageUrl,
title = "ranked content"
)
)
private fun audioContentNews(
newsId: String = "news",
visibleFromAtUtc: String = "2026-06-25T01:00:00Z",
contentImageUrl: String? = "https://example.com/audio.png"
) = FollowingNewsResponse(
newsId = newsId,
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = visibleFromAtUtc,
audioContent = FollowingContentNewsResponse(
contentId = 3001L,
contentImageUrl = contentImageUrl,
title = "audio title",
creatorProfileImageUrl = "https://example.com/audio-creator.png",
creatorNickname = "audio creator"
)
)
private fun photoContentNews(
newsId: String = "news",
visibleFromAtUtc: String = "2026-06-25T01:00:00Z"
) = FollowingNewsResponse(
newsId = newsId,
type = FollowingNewsType.PHOTO_CONTENT,
visibleFromAtUtc = visibleFromAtUtc,
photoContent = FollowingContentNewsResponse(
contentId = 3002L,
contentImageUrl = "https://example.com/photo.png",
title = "photo title",
creatorProfileImageUrl = "https://example.com/photo-creator.png",
creatorNickname = "photo creator"
)
)
private fun communityPostNews(
newsId: String = "news",
visibleFromAtUtc: String = "2026-06-25T01:00:00Z"
) = FollowingNewsResponse(
newsId = newsId,
type = FollowingNewsType.COMMUNITY_POST,
visibleFromAtUtc = visibleFromAtUtc,
communityPost = FollowingCommunityPostNewsResponse(
postId = 4001L,
creatorProfileImage = "https://example.com/community-creator.png",
creatorNickname = "community creator",
imageUrl = "https://example.com/community-image.png",
content = "community body",
createdAt = "2026-06-25T02:00:00Z",
likeCount = 11,
commentCount = 5
)
)
}