feat(creator): 라이브 탭 레이아웃을 추가한다

This commit is contained in:
2026-06-17 23:24:13 +09:00
parent dd13c619ac
commit 7fb52b3c85
6 changed files with 926 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.v2.creator.channel
import android.app.Application
import androidx.fragment.app.FragmentActivity
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelPagerAdapterTest {
@Test
fun `createFragment는 Home과 Live를 실제 Fragment로 생성하고 나머지는 placeholder를 유지한다`() {
val activity = Robolectric.buildActivity(FragmentActivity::class.java).setup().get()
val adapter = CreatorChannelPagerAdapter(activity, creatorId = 123L)
assertTrue(adapter.createFragment(CreatorChannelTab.Home.ordinal) is CreatorChannelHomeFragment)
assertTrue(adapter.createFragment(CreatorChannelTab.Live.ordinal) is CreatorChannelLiveFragment)
CreatorChannelTab.entries
.filterNot { it == CreatorChannelTab.Home || it == CreatorChannelTab.Live }
.forEach { tab ->
assertTrue(adapter.createFragment(tab.ordinal) is CreatorChannelPlaceholderFragment)
}
assertEquals(CreatorChannelTab.entries.size, adapter.itemCount)
}
}

View File

@@ -0,0 +1,183 @@
package kr.co.vividnext.sodalive.v2.creator.channel.live
import android.app.Application
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class CreatorChannelLiveFragmentLayoutTest {
@Test
fun `라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`() {
val root = inflateView(R.layout.fragment_creator_channel_live)
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_sort_bar))
val currentLiveCard = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_current_card))
val replayList = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_creator_channel_live_replays))
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_live_empty_message))
val errorMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_live_error_message))
val retryButton = requireNotNull(root.findViewById<TextView>(R.id.btn_creator_channel_live_retry))
val ownerCta = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_owner_cta))
assertSame(root, sortBar.parent)
assertSame(root, replayList.parent)
assertSame(root, emptyMessage.parent)
assertSame(root, errorMessage.parent)
assertSame(root, retryButton.parent)
assertSame(root, ownerCta.parent)
assertNotNull(currentLiveCard.findViewById<TextView>(R.id.tv_creator_channel_live_current_title))
assertEquals(false, replayList.clipToPadding)
}
@Test
fun `라이브 sort bar는 전체 count 정렬 label sort icon을 제공한다`() {
val root = inflateView(R.layout.fragment_creator_channel_live)
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_sort_bar))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_live_total_label))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_live_total_count))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_live_sort_label))
assertNotNull(sortBar.findViewById<ImageView>(R.id.iv_creator_channel_live_sort))
}
@Test
fun `라이브 다시듣기 item layout은 썸네일 tag title duration action 영역을 제공한다`() {
val item = inflateView(R.layout.item_creator_channel_live_replay)
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_thumbnail))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_adult_badge))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_original_tag))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_first_tag))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_point_tag))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_live_replay_free_tag))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_live_replay_title))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_live_replay_duration))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_action))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_play))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_can))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_action_text))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_live_replay_action_text))
}
@Test
fun `라이브 layout은 가격 영역을 유연한 너비와 bar cash icon으로 제공한다`() {
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText()
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_live_replay.xml").readText()
val root = inflateView(R.layout.fragment_creator_channel_live)
val item = inflateView(R.layout.item_creator_channel_live_replay)
val currentPrice = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_live_current_price))
val currentPriceCash = requireNotNull(root.findViewById<ImageView>(R.id.iv_creator_channel_live_current_price_cash))
val replayAction = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_action))
val replayPlay = requireNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_play))
val actionText = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_live_replay_action_text))
val adultBadge = requireNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_live_replay_adult_badge))
val adultBadgeParams = adultBadge.layoutParams as ViewGroup.MarginLayoutParams
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, currentPrice.layoutParams.width)
assertNotNull(currentPriceCash)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, replayAction.layoutParams.width)
assertEquals(dp(28), replayPlay.layoutParams.width)
assertEquals(dp(28), replayPlay.layoutParams.height)
assertEquals(dp(6), replayPlay.paddingStart)
assertEquals(dp(8), (actionText.layoutParams as ViewGroup.MarginLayoutParams).topMargin)
assertEquals(dp(6), adultBadgeParams.topMargin)
assertEquals(dp(6), adultBadgeParams.marginEnd)
assertTrue(fragmentLayout.contains("@drawable/ic_bar_cash"))
assertTrue(itemLayout.contains("@drawable/ic_bar_cash"))
assertTrue(itemLayout.contains("@drawable/bg_creator_channel_live_replay_play"))
assertTrue(itemLayout.contains("android:orientation=\"vertical\""))
assertFalse(fragmentLayout.contains("@drawable/ic_can"))
assertFalse(itemLayout.contains("@drawable/ic_can"))
assertFalse(
fragmentLayout.contains(
"android:id=\"@+id/layout_creator_channel_live_current_price\"\n" +
" android:layout_width=\"60dp\""
)
)
assertFalse(
itemLayout.contains(
"android:id=\"@+id/layout_creator_channel_live_replay_action\"\n" +
" android:layout_width=\"60dp\""
)
)
}
@Test
fun `라이브 fragment와 adapter source는 필수 drawable과 retry loadMore click 연결을 포함한다`() {
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt"
).readText()
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_live.xml").readText()
val eagerLoadOnViewCreated = """
onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupReplayList()
setupClickListeners()
observeViewModel()
if (creatorId > 0L)
""".trimIndent()
assertTrue(layout.contains("android:layout_height=\"match_parent\""))
assertTrue(fragment.contains("R.drawable.ic_new_sort"))
assertTrue(fragment.contains("super.onViewCreated(view, savedInstanceState)\n bindLoading()"))
assertTrue(fragment.contains("retryLive()"))
assertTrue(fragment.contains("fun onCreatorChannelLiveTabSelected()"))
assertTrue(fragment.contains("viewModel.loadLive(creatorId)"))
assertFalse(fragment.contains(eagerLoadOnViewCreated))
assertTrue(fragment.contains("fun onCreatorChannelLiveScrolledToBottom()"))
assertTrue(fragment.contains("viewModel.loadMore()"))
assertTrue(fragment.contains("host.onCreatorChannelLiveContentChanged()"))
assertTrue(fragment.contains("notifyContentChangedIfLayoutChanged(state)"))
assertTrue(fragment.contains("if (contentLayoutKey == lastContentLayoutKey) return"))
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
assertTrue(fragment.contains("bindEmpty() = with(binding)"))
assertTrue(fragment.contains("bindError(state: CreatorChannelLiveUiState.Error) = with(binding)"))
assertTrue(fragment.contains("formatCreatorChannelLiveDateTime(live.beginDateTimeUtc)"))
assertTrue(fragment.contains("ivCreatorChannelLiveCurrentPriceCash.isVisible = live.price > 0"))
assertTrue(fragment.contains("R.string.audio_content_tag_free"))
assertTrue(fragment.contains("onCreatorChannelCurrentLiveClicked"))
assertTrue(fragment.contains("onCreatorChannelLiveContentChanged"))
assertFalse(fragment.contains("addOnScrollListener(object : RecyclerView.OnScrollListener()"))
assertTrue(adapter.contains("R.drawable.ic_new_shield_small"))
assertTrue(adapter.contains("R.drawable.ic_new_player_play"))
assertTrue(adapter.contains("ivCreatorChannelLiveReplayPlay.isVisible = true"))
assertTrue(adapter.contains("layoutCreatorChannelLiveReplayActionText.isVisible = false"))
assertTrue(adapter.contains("layoutCreatorChannelLiveReplayActionText.isVisible = true"))
}
private fun inflateView(layoutResId: Int): View {
val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false)
}
private fun dp(value: Int): Int {
val context = ApplicationProvider.getApplicationContext<Context>()
return (value * context.resources.displayMetrics.density).toInt()
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath")
}
}