feat(creator): 시리즈 탭 UI를 구현한다

This commit is contained in:
2026-06-20 04:50:30 +09:00
parent 7ea06fda2f
commit 92cea6d3ee
5 changed files with 717 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
package kr.co.vividnext.sodalive.v2.creator.channel.series
import android.app.Application
import android.content.Context
import android.view.LayoutInflater
import android.view.View
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.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 CreatorChannelSeriesFragmentLayoutTest {
@Test
fun `시리즈 fragment layout은 sort list empty error retry를 제공한다`() {
val root = inflateView(R.layout.fragment_creator_channel_series)
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_series.xml").readText()
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_series_sort_bar))
val seriesList = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_creator_channel_series))
val emptyContainer = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_series_empty))
val emptyMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_series_empty_message))
val errorMessage = requireNotNull(root.findViewById<TextView>(R.id.tv_creator_channel_series_error_message))
val retryButton = requireNotNull(root.findViewById<TextView>(R.id.btn_creator_channel_series_retry))
assertSame(root, sortBar.parent)
assertSame(root, seriesList.parent)
assertSame(root, emptyContainer.parent)
assertSame(emptyContainer, emptyMessage.parent)
assertSame(root, errorMessage.parent)
assertSame(root, retryButton.parent)
assertEquals(false, seriesList.clipToPadding)
assertTrue(layout.contains("android:background=\"@color/black\""))
assertTrue(layout.contains("android:text=\"@string/creator_channel_series_empty_message\""))
assertTrue(layout.contains("android:text=\"@string/creator_channel_series_error_message\""))
assertTrue(layout.contains("tools:listitem=\"@layout/item_creator_channel_series\""))
}
@Test
fun `시리즈 sort bar는 전체 count 정렬 label sort icon을 제공한다`() {
val root = inflateView(R.layout.fragment_creator_channel_series)
val sortBar = requireNotNull(root.findViewById<View>(R.id.layout_creator_channel_series_sort_bar))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_series_total_label))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_series_total_count))
assertNotNull(sortBar.findViewById<View>(R.id.layout_creator_channel_series_sort_button))
assertNotNull(sortBar.findViewById<TextView>(R.id.tv_creator_channel_series_sort_label))
assertNotNull(sortBar.findViewById<ImageView>(R.id.iv_creator_channel_series_sort))
}
@Test
fun `시리즈 item layout은 thumbnail info progress를 제공하고 우측 action을 만들지 않는다`() {
val item = inflateView(R.layout.item_creator_channel_series)
val itemLayout = projectFile("app/src/main/res/layout/item_creator_channel_series.xml").readText()
val thumbnail = requireNotNull(item.findViewById<View>(R.id.layout_creator_channel_series_thumbnail))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_series_thumbnail))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_series_original_tag))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_series_adult_badge))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_series_title))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_series_subtitle))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_series_progress))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_series_progress_count))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_series_progress_percent))
assertNotNull(item.findViewById<View>(R.id.view_creator_channel_series_progress_fill))
assertEquals(dp(122), thumbnail.layoutParams.width)
assertEquals(dp(172), thumbnail.layoutParams.height)
assertTrue(!itemLayout.contains("전체소장"))
assertTrue(!itemLayout.contains("button-play"))
assertTrue(!itemLayout.contains("iv_creator_channel_series_play"))
}
@Test
fun `시리즈 empty 문자열은 한국어 영어 일본어에 존재한다`() {
val ko = projectFile("app/src/main/res/values/strings.xml").readText()
val en = projectFile("app/src/main/res/values-en/strings.xml").readText()
val ja = projectFile("app/src/main/res/values-ja/strings.xml").readText()
assertTrue(ko.contains("name=\"creator_channel_series_empty_message\""))
assertTrue(en.contains("name=\"creator_channel_series_empty_message\""))
assertTrue(ja.contains("name=\"creator_channel_series_empty_message\""))
}
@Test
fun `시리즈 fragment source는 Audio 탭과 같은 sort pagination content change 계약을 사용한다`() {
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/CreatorChannelSeriesFragment.kt"
).readText()
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/series/ui/CreatorChannelSeriesAdapter.kt"
).readText()
assertTrue(fragment.contains("BaseFragment<FragmentCreatorChannelSeriesBinding>"))
assertTrue(fragment.contains("private val viewModel: CreatorChannelSeriesViewModel by viewModel()"))
assertTrue(fragment.contains("fun onCreatorChannelSeriesTabSelected()"))
assertTrue(fragment.contains("viewModel.loadSeries(creatorId, isOwner = host.isCreatorChannelOwner())"))
assertTrue(fragment.contains("fun onCreatorChannelSeriesScrolledToBottom()"))
assertTrue(fragment.contains("viewModel.loadMore()"))
assertTrue(fragment.contains("CreatorChannelSortPopup"))
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
assertTrue(fragment.contains("notifyContentChangedIfLayoutChanged(state)"))
assertTrue(adapter.contains("ItemCreatorChannelSeriesBinding"))
assertTrue(adapter.contains("layoutCreatorChannelSeriesThumbnail.clipToOutline = true"))
assertTrue(adapter.contains("formatSubtitle(item.subtitle)"))
assertTrue(adapter.contains("R.string.creator_channel_series_subtitle_content_count"))
assertTrue(adapter.contains("R.string.creator_channel_series_status_proceeding"))
assertTrue(adapter.contains("R.string.creator_channel_series_status_completed"))
assertTrue(!adapter.contains("tvCreatorChannelSeriesSubtitle.text = item.subtitle"))
}
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")
}
}