feat(content): 추천 API 상태를 화면에 바인딩한다
This commit is contained in:
@@ -1,14 +1,55 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.content
|
package kr.co.vividnext.sodalive.v2.main.content
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentBannerUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentBannerBinder
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentCommentedAudioAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentNewAndHotAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentOriginalSeriesAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentHorizontalItemSpacing
|
||||||
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||||
FragmentV2MainContentBinding::inflate
|
FragmentV2MainContentBinding::inflate
|
||||||
) {
|
) {
|
||||||
|
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
||||||
|
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||||
|
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
||||||
|
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||||
|
private val newAndHotAdapter = ContentNewAndHotAdapter { openAudioContentDetail(it) }
|
||||||
|
private val freeAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||||
|
private val pointAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||||
|
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
||||||
|
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||||
|
private var bannerBinder: ContentBannerBinder? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.textTabBarContent.root.setMenus(
|
binding.textTabBarContent.root.setMenus(
|
||||||
@@ -16,29 +57,188 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
)
|
)
|
||||||
setUpSectionTitles()
|
setUpSectionTitles()
|
||||||
|
setUpAdapters()
|
||||||
|
bindObservers()
|
||||||
|
contentMainViewModel.loadRecommendations()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpAdapters() {
|
||||||
|
bannerBinder = ContentBannerBinder(binding.rvContentBanners).apply {
|
||||||
|
setOnBannerClick { onBannerClick(it) }
|
||||||
|
}
|
||||||
|
binding.rvContentOriginalSeries.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = originalSeriesAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentLatestAudios.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = latestAudioAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentNewAndHotAudios.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = newAndHotAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentFreeAudios.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = freeAudioAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentPointAudios.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = pointAudioAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentMostCommentedAudios.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = commentedAudioAdapter
|
||||||
|
addContentHorizontalItemSpacing()
|
||||||
|
}
|
||||||
|
binding.rvContentRecommendedAudios.apply {
|
||||||
|
layoutManager = GridLayoutManager(requireContext(), CONTENT_RECOMMENDED_GRID_SPAN_COUNT)
|
||||||
|
adapter = recommendedAudioAdapter
|
||||||
|
addContentGridItemSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindObservers() {
|
||||||
|
contentMainViewModel.recommendationsStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||||
|
when (state) {
|
||||||
|
is AudioRecommendationsUiState.Content -> bindContent(state)
|
||||||
|
AudioRecommendationsUiState.Empty,
|
||||||
|
is AudioRecommendationsUiState.Error -> bindContent(emptyContent())
|
||||||
|
|
||||||
|
AudioRecommendationsUiState.Loading -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentMainViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
|
if (isLoading) {
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentMainViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||||
|
toastMessage?.let(::showToast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindContent(content: AudioRecommendationsUiState.Content) {
|
||||||
|
bindBannerSection(content.banners)
|
||||||
|
bindOriginalSeriesSection(content.originalSeries)
|
||||||
|
bindLatestAudioSection(content.latestAudios)
|
||||||
|
bindNewAndHotSection(content.newAndHotAudios)
|
||||||
|
bindFreeAudioSection(content.freeAudios)
|
||||||
|
bindPointAudioSection(content.pointAudios)
|
||||||
|
bindCommentedAudioSection(content.mostCommentedAudios)
|
||||||
|
bindRecommendedAudioSection(content.recommendedAudios)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindBannerSection(section: ContentBannerSection) {
|
||||||
|
binding.llContentBannerSection.visibility = section.items.toSectionVisibility()
|
||||||
|
bannerBinder?.bind(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindOriginalSeriesSection(section: ContentOriginalSeriesSection) {
|
||||||
|
binding.llContentOriginalSeriesSection.visibility = section.items.toSectionVisibility()
|
||||||
|
originalSeriesAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindLatestAudioSection(section: ContentAudioCardSection) {
|
||||||
|
binding.llContentLatestAudioSection.visibility = section.items.toSectionVisibility()
|
||||||
|
latestAudioAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindNewAndHotSection(section: ContentAudioCardSection) {
|
||||||
|
binding.llContentNewAndHotSection.visibility = section.items.toSectionVisibility()
|
||||||
|
newAndHotAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindFreeAudioSection(section: ContentAudioCardSection) {
|
||||||
|
binding.llContentFreeAudioSection.visibility = section.items.toSectionVisibility()
|
||||||
|
freeAudioAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindPointAudioSection(section: ContentAudioCardSection) {
|
||||||
|
binding.llContentPointAudioSection.visibility = section.items.toSectionVisibility()
|
||||||
|
pointAudioAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindCommentedAudioSection(section: ContentCommentedAudioSection) {
|
||||||
|
binding.llContentMostCommentedAudioSection.visibility = section.items.toSectionVisibility()
|
||||||
|
commentedAudioAdapter.submitItems(section.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindRecommendedAudioSection(section: ContentAudioCardSection) {
|
||||||
|
binding.llContentRecommendedAudioSection.visibility = section.items.toSectionVisibility()
|
||||||
|
recommendedAudioAdapter.submitItems(section.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpSectionTitles() {
|
private fun setUpSectionTitles() {
|
||||||
binding.viewContentOriginalSeriesTitle.tvSectionTitle.setText(
|
binding.viewContentOriginalSeriesTitle.setTitle(R.string.content_recommendation_section_original_series)
|
||||||
R.string.content_recommendation_section_original_series
|
binding.viewContentLatestAudioTitle.setTitle(R.string.content_recommendation_section_latest_audio)
|
||||||
)
|
binding.viewContentNewAndHotTitle.setTitle(R.string.content_recommendation_section_new_and_hot)
|
||||||
binding.viewContentLatestAudioTitle.tvSectionTitle.setText(
|
binding.viewContentFreeAudioTitle.setTitle(R.string.content_recommendation_section_free_audio)
|
||||||
R.string.content_recommendation_section_latest_audio
|
binding.viewContentPointAudioTitle.setTitle(R.string.content_recommendation_section_point_audio)
|
||||||
)
|
binding.viewContentMostCommentedAudioTitle.setTitle(R.string.content_recommendation_section_most_commented_audio)
|
||||||
binding.viewContentNewAndHotTitle.tvSectionTitle.setText(
|
binding.viewContentRecommendedAudioTitle.setTitle(R.string.content_recommendation_section_recommended_audio)
|
||||||
R.string.content_recommendation_section_new_and_hot
|
}
|
||||||
)
|
|
||||||
binding.viewContentFreeAudioTitle.tvSectionTitle.setText(
|
private fun ViewSectionTitleBinding.setTitle(titleResId: Int) {
|
||||||
R.string.content_recommendation_section_free_audio
|
tvSectionTitle.setText(titleResId)
|
||||||
)
|
ivSectionTitleChevron.visibility = View.GONE
|
||||||
binding.viewContentPointAudioTitle.tvSectionTitle.setText(
|
}
|
||||||
R.string.content_recommendation_section_point_audio
|
|
||||||
)
|
private fun onBannerClick(item: ContentBannerUiModel) {
|
||||||
binding.viewContentMostCommentedAudioTitle.tvSectionTitle.setText(
|
val route = item.toContentBannerRoute() ?: return
|
||||||
R.string.content_recommendation_section_most_commented_audio
|
startActivity(route.toContentBannerIntent(requireContext()))
|
||||||
)
|
}
|
||||||
binding.viewContentRecommendedAudioTitle.tvSectionTitle.setText(
|
|
||||||
R.string.content_recommendation_section_recommended_audio
|
private fun openAudioContentDetail(item: ContentAudioCardUiModel) {
|
||||||
|
openAudioContentDetail(item.audioContentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAudioContentDetail(item: ContentCommentedAudioUiModel) {
|
||||||
|
openAudioContentDetail(item.audioContentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAudioContentDetail(audioContentId: Long) {
|
||||||
|
if (audioContentId <= 0L) return
|
||||||
|
startActivity(
|
||||||
|
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||||
|
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
||||||
|
startActivity(
|
||||||
|
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(toastMessage: ToastMessage) {
|
||||||
|
toastMessage.message?.let { message -> showToast(message) }
|
||||||
|
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyContent(): AudioRecommendationsUiState.Content {
|
||||||
|
return AudioRecommendationsUiState.Content(
|
||||||
|
banners = ContentBannerSection(emptyList()),
|
||||||
|
originalSeries = ContentOriginalSeriesSection(emptyList()),
|
||||||
|
latestAudios = ContentAudioCardSection(emptyList()),
|
||||||
|
newAndHotAudios = ContentAudioCardSection(emptyList()),
|
||||||
|
freeAudios = ContentAudioCardSection(emptyList()),
|
||||||
|
pointAudios = ContentAudioCardSection(emptyList()),
|
||||||
|
mostCommentedAudios = ContentCommentedAudioSection(emptyList()),
|
||||||
|
recommendedAudios = ContentAudioCardSection(emptyList())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<*>.toSectionVisibility(): Int = if (isEmpty()) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.content.ui
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
|
||||||
|
fun RecyclerView.addContentHorizontalItemSpacing() {
|
||||||
|
if (itemDecorationCount == 0) addItemDecoration(ContentHorizontalItemDecoration())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.addContentGridItemSpacing() {
|
||||||
|
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContentHorizontalItemDecoration : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
if (position != RecyclerView.NO_POSITION && position < (parent.adapter?.itemCount ?: 0) - 1) {
|
||||||
|
outRect.right = HORIZONTAL_ITEM_GAP_DP.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContentGridItemDecoration : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
if (position == RecyclerView.NO_POSITION) return
|
||||||
|
val isLeftColumn = position % GRID_SPAN_COUNT == 0
|
||||||
|
val halfGap = GRID_ITEM_GAP_DP.dpToPx().toInt() / 2
|
||||||
|
outRect.left = if (isLeftColumn) 0 else halfGap
|
||||||
|
outRect.right = if (isLeftColumn) halfGap else 0
|
||||||
|
if (position >= GRID_SPAN_COUNT) outRect.top = GRID_ITEM_VERTICAL_GAP_DP.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val HORIZONTAL_ITEM_GAP_DP = 8
|
||||||
|
private const val GRID_ITEM_GAP_DP = 8
|
||||||
|
private const val GRID_ITEM_VERTICAL_GAP_DP = 28
|
||||||
|
const val CONTENT_RECOMMENDED_GRID_SPAN_COUNT = 2
|
||||||
|
private const val GRID_SPAN_COUNT = CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
||||||
@@ -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