feat(banner): Phase 8 배너 동작을 보완한다

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-28 13:10:38 +09:00
parent df782d7968
commit 8dd2371ce4
3 changed files with 151 additions and 9 deletions

View File

@@ -8,6 +8,8 @@ import android.view.View.MeasureSpec
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
@@ -97,10 +99,30 @@ class BannerViewTest {
assertEquals(View.VISIBLE, view.visibility)
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.layout_banner_counter).visibility)
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
assertEquals("/", view.findViewById<TextView>(R.id.tv_banner_counter_separator).text.toString())
assertEquals(" / ", view.findViewById<TextView>(R.id.tv_banner_counter_separator).text.toString())
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_total_count).text.toString())
}
@Test
fun `배너 view counter는 PRD 시각 형식과 색상 및 14sp 크기를 유지한다`() {
val view = inflateBannerView()
view.setItems(listOf(sampleItem("1"), sampleItem("2")))
val currentIndex = view.findViewById<TextView>(R.id.tv_banner_current_index)
val separator = view.findViewById<TextView>(R.id.tv_banner_counter_separator)
val totalCount = view.findViewById<TextView>(R.id.tv_banner_total_count)
val displayedCounter = currentIndex.text.toString() + separator.text + totalCount.text
assertEquals("01 / 02", displayedCounter)
assertEquals(view.resources.getColor(R.color.white), currentIndex.currentTextColor)
assertEquals(view.resources.getColor(R.color.gray_400), separator.currentTextColor)
assertEquals(view.resources.getColor(R.color.gray_400), totalCount.currentTextColor)
assertEquals(14f, currentIndex.textSize / view.resources.displayMetrics.scaledDensity)
assertEquals(14f, separator.textSize / view.resources.displayMetrics.scaledDensity)
assertEquals(14f, totalCount.textSize / view.resources.displayMetrics.scaledDensity)
}
@Test
fun `배너 view는 preview 속성의 count와 current index를 counter에 반영한다`() {
val context = ApplicationProvider.getApplicationContext<Context>()
@@ -293,6 +315,27 @@ class BannerViewTest {
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
}
@Test
fun `배너 view는 lifecycle stop에서 자동 이동을 멈추고 start에서 다시 예약한다`() {
val owner = TestLifecycleOwner()
val view = inflateBannerView()
view.setTag(lifecycleOwnerTagId(), owner)
view.setItems(listOf(sampleItem("1"), sampleItem("2")))
view.measure(exactly(402.dpToPx()), exactly(402.dpToPx()))
view.layout(0, 0, 402.dpToPx(), 402.dpToPx())
view.dispatchAttachedForTest()
owner.handleEvent(Lifecycle.Event.ON_START)
owner.handleEvent(Lifecycle.Event.ON_STOP)
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
owner.handleEvent(Lifecycle.Event.ON_START)
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
}
@Test
fun `배너 view는 단일 item이면 attach 후 5초가 지나도 자동 이동하지 않는다`() {
val view = inflateBannerView()
@@ -346,6 +389,47 @@ class BannerViewTest {
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
}
@Test
fun `배너 view는 새 목록을 설정하면 첫 번째 배너 기준으로 다시 시작한다`() {
val view = inflateBannerView()
view.setItems(listOf(sampleItem("1"), sampleItem("2"), sampleItem("3")))
view.measure(exactly(402.dpToPx()), exactly(402.dpToPx()))
view.layout(0, 0, 402.dpToPx(), 402.dpToPx())
view.dispatchAttachedForTest()
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
view.setItems(listOf(sampleItem("4"), sampleItem("5"), sampleItem("6")))
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
}
@Test
fun `배너 view는 수동 경계와 빠른 위치 변경 후 idle에서 counter를 실제 index와 동기화한다`() {
val view = inflateBannerView()
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_banner)
view.setItems(listOf(sampleItem("1"), sampleItem("2"), sampleItem("3")))
val adapter = requireNotNull(recyclerView.adapter as? BannerAdapter)
val firstPosition = adapter.startPosition(0)
val lastPosition = adapter.startPosition(2)
view.setCurrentPositionForTest(firstPosition - 1)
view.dispatchScrollStateForTest(RecyclerView.SCROLL_STATE_IDLE)
assertEquals("03", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
view.setCurrentPositionForTest(lastPosition + 1)
view.dispatchScrollStateForTest(RecyclerView.SCROLL_STATE_IDLE)
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
view.setCurrentPositionForTest(firstPosition + 5)
view.dispatchScrollStateForTest(RecyclerView.SCROLL_STATE_IDLE)
assertEquals("03", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
}
private fun inflateBannerView(): BannerView {
val context = ApplicationProvider.getApplicationContext<Context>()
return BannerView(context)
@@ -372,6 +456,7 @@ class BannerViewTest {
}
private fun BannerView.setCurrentPositionForTest(position: Int) {
findViewById<RecyclerView>(R.id.rv_banner).scrollToPosition(position)
BannerView::class.java.getDeclaredField("currentAdapterPosition").apply {
isAccessible = true
setInt(this@setCurrentPositionForTest, position)
@@ -407,4 +492,20 @@ class BannerViewTest {
val context = ApplicationProvider.getApplicationContext<Context>()
return (this * context.resources.displayMetrics.density).toInt()
}
private fun lifecycleOwnerTagId(): Int {
return Class.forName("androidx.lifecycle.runtime.R\$id")
.getDeclaredField("view_tree_lifecycle_owner")
.getInt(null)
}
private class TestLifecycleOwner : androidx.lifecycle.LifecycleOwner {
private val registry = LifecycleRegistry(this)
override val lifecycle: Lifecycle = registry
fun handleEvent(event: Lifecycle.Event) {
registry.handleLifecycleEvent(event)
}
}
}