feat(content): 추천 API 상태를 화면에 바인딩한다
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.content
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerRoute
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerUiModel
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class ContentMainFragmentSourceTest {
|
||||
|
||||
@Test
|
||||
fun `ContentMainFragment는 Phase 4~6 adapter와 ViewModel observer를 연결한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("private val contentMainViewModel: ContentMainViewModel by viewModel()"))
|
||||
assertTrue(source.contains("ContentBannerBinder(binding.rvContentBanners)"))
|
||||
assertTrue(source.contains("ContentOriginalSeriesAdapter"))
|
||||
assertTrue(source.contains("ContentAudioCardAdapter"))
|
||||
assertTrue(source.contains("ContentNewAndHotAdapter"))
|
||||
assertTrue(source.contains("ContentCommentedAudioAdapter"))
|
||||
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
|
||||
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))
|
||||
assertTrue(source.contains("LoadingDialog(requireActivity(), layoutInflater)"))
|
||||
assertTrue(source.contains("loadingDialog.show(screenWidth)"))
|
||||
assertTrue(source.contains("toastMessage?.let(::showToast)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
|
||||
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)"))
|
||||
assertTrue(source.contains("SeriesDetailActivity::class.java"))
|
||||
assertTrue(source.contains("putExtra(Constants.EXTRA_SERIES_ID, seriesId)"))
|
||||
assertTrue(source.contains("toContentBannerRoute()"))
|
||||
assertTrue(source.contains("toContentBannerIntent(requireContext())"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content 추천 layout은 Phase 4~6 item layout과 제외 섹션 정책을 지킨다`() {
|
||||
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_v2_main_content.xml").readText()
|
||||
val audioCardLayout = projectFile("app/src/main/res/layout/view_audio_content_card.xml").readText()
|
||||
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_banners"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_original_series"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_latest_audios"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_new_and_hot_audios"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_free_audios"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_point_audios"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_most_commented_audios"))
|
||||
assertTrue(fragmentLayout.contains("@+id/rv_content_recommended_audios"))
|
||||
assertFalse(fragmentLayout.contains("recommendation_series"))
|
||||
assertFalse(fragmentLayout.contains("keyword_audio"))
|
||||
assertTrue(audioCardLayout.contains("@+id/iv_audio_content_adult_badge"))
|
||||
assertTrue(audioCardLayout.contains("android:visibility=\"gone\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Phase 4~5 adapter source는 grouping과 badge comment 정책을 포함한다`() {
|
||||
val cardView = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt"
|
||||
).readText()
|
||||
val newAndHotAdapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentNewAndHotAdapter.kt"
|
||||
).readText()
|
||||
val commentedAdapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentCommentedAudioAdapter.kt"
|
||||
).readText()
|
||||
|
||||
assertTrue(cardView.contains("fun setAdultVisible(isVisible: Boolean)"))
|
||||
assertTrue(newAndHotAdapter.contains("items.chunked(NEW_AND_HOT_GROUP_SIZE)"))
|
||||
assertTrue(newAndHotAdapter.contains("private const val NEW_AND_HOT_GROUP_SIZE = 3"))
|
||||
assertTrue(commentedAdapter.contains("layoutContentCommentArea.isVisible = item.showLatestComment"))
|
||||
assertTrue(commentedAdapter.contains("ivContentCommentProfile.loadUrl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content banner route uses event creator series link priority`() {
|
||||
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
||||
|
||||
assertEquals(
|
||||
ContentBannerRoute.Event(eventItem),
|
||||
banner(
|
||||
eventItem = eventItem,
|
||||
creatorId = 2L,
|
||||
seriesId = 3L,
|
||||
link = "https://example.com"
|
||||
).toContentBannerRoute()
|
||||
)
|
||||
assertEquals(
|
||||
ContentBannerRoute.Creator(2L),
|
||||
banner(creatorId = 2L, seriesId = 3L, link = "https://example.com").toContentBannerRoute()
|
||||
)
|
||||
assertEquals(
|
||||
ContentBannerRoute.Series(3L),
|
||||
banner(seriesId = 3L, link = "https://example.com").toContentBannerRoute()
|
||||
)
|
||||
assertEquals(
|
||||
ContentBannerRoute.Link("https://example.com", isWebUrl = true),
|
||||
banner(link = "https://example.com").toContentBannerRoute()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content banner route creates expected intents`() {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png")
|
||||
val eventIntent = ContentBannerRoute.Event(eventItem).toContentBannerIntent(context)
|
||||
val creatorIntent = ContentBannerRoute.Creator(2L).toContentBannerIntent(context)
|
||||
val seriesIntent = ContentBannerRoute.Series(3L).toContentBannerIntent(context)
|
||||
val webIntent = ContentBannerRoute.Link("https://example.com", isWebUrl = true).toContentBannerIntent(context)
|
||||
val deepLinkIntent = ContentBannerRoute.Link(
|
||||
url = "${BuildConfig.APPSCHEME}://series/3",
|
||||
isWebUrl = false
|
||||
).toContentBannerIntent(context)
|
||||
|
||||
assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className)
|
||||
assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT))
|
||||
assertEquals(CreatorChannelActivity::class.java.name, creatorIntent.component?.className)
|
||||
assertEquals(2L, creatorIntent.getLongExtra(CreatorChannelActivity.EXTRA_CREATOR_ID, 0L))
|
||||
assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className)
|
||||
assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L))
|
||||
assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action)
|
||||
assertEquals("https://example.com", webIntent.data.toString())
|
||||
assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action)
|
||||
assertEquals("${BuildConfig.APPSCHEME}://series/3", deepLinkIntent.data.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content banner route ignores invalid links`() {
|
||||
assertEquals(null, banner(link = "").toContentBannerRoute())
|
||||
assertEquals(null, banner(link = "not a url").toContentBannerRoute())
|
||||
assertEquals(null, banner(link = "example.com/path").toContentBannerRoute())
|
||||
assertEquals(null, banner(link = "mailto:test@example.com").toContentBannerRoute())
|
||||
}
|
||||
|
||||
private fun banner(
|
||||
eventItem: EventItem? = null,
|
||||
creatorId: Long? = null,
|
||||
seriesId: Long? = null,
|
||||
link: String? = null
|
||||
) = ContentBannerUiModel(
|
||||
imageUrl = "https://example.com/banner.png",
|
||||
eventItem = eventItem,
|
||||
creatorId = creatorId,
|
||||
seriesId = seriesId,
|
||||
link = link
|
||||
)
|
||||
|
||||
private fun projectFile(relativePath: String): File {
|
||||
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||
return candidates.firstOrNull { it.exists() }
|
||||
?: error("Project file not found: $relativePath")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user