feat(banner): 배너 자동 전환 동작을 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -18,7 +18,9 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import java.time.Duration
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
@@ -148,11 +150,152 @@ class BannerViewTest {
|
||||
assertNotNull(imageView.outlineProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 attach 후 5초가 지나면 다음 배너로 자동 이동한다`() {
|
||||
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())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 사용자 drag 중에는 자동 이동 timer를 다시 시작하지 않는다`() {
|
||||
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()
|
||||
view.dispatchScrollStateForTest(RecyclerView.SCROLL_STATE_DRAGGING)
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
|
||||
|
||||
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
|
||||
view.dispatchScrollStateForTest(RecyclerView.SCROLL_STATE_IDLE)
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(4_999))
|
||||
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(1))
|
||||
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 detach 되면 자동 이동 callback을 제거한다`() {
|
||||
val view = inflateBannerView()
|
||||
|
||||
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()
|
||||
view.dispatchDetachedForTest()
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
|
||||
|
||||
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 단일 item이면 attach 후 5초가 지나도 자동 이동하지 않는다`() {
|
||||
val view = inflateBannerView()
|
||||
|
||||
view.setItems(listOf(sampleItem("1")))
|
||||
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(View.GONE, view.findViewById<View>(R.id.layout_banner_counter).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 마지막 배너 다음에 첫 번째 counter로 순환한다`() {
|
||||
val view = inflateBannerView()
|
||||
|
||||
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()
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
|
||||
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
|
||||
|
||||
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 자동 전환 animation duration을 350ms로 사용한다`() {
|
||||
assertEquals(350, BannerView.scrollAnimationDurationMsForTest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `배너 view는 현재 virtual position 기준으로 다음 배너로 이동한다`() {
|
||||
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 previousVirtualPosition = adapter.startPosition(0) - 1
|
||||
view.measure(exactly(402.dpToPx()), exactly(402.dpToPx()))
|
||||
view.layout(0, 0, 402.dpToPx(), 402.dpToPx())
|
||||
view.setCurrentPositionForTest(previousVirtualPosition)
|
||||
view.dispatchAttachedForTest()
|
||||
|
||||
shadowOf(android.os.Looper.getMainLooper()).idleFor(Duration.ofMillis(5_000))
|
||||
|
||||
assertEquals(previousVirtualPosition + 1, view.currentAdapterPositionForTest())
|
||||
assertEquals("01", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||
}
|
||||
|
||||
private fun inflateBannerView(): BannerView {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return BannerView(context)
|
||||
}
|
||||
|
||||
private fun BannerView.dispatchAttachedForTest() {
|
||||
BannerView::class.java.getDeclaredMethod("onAttachedToWindow").apply {
|
||||
isAccessible = true
|
||||
invoke(this@dispatchAttachedForTest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BannerView.dispatchDetachedForTest() {
|
||||
BannerView::class.java.getDeclaredMethod("onDetachedFromWindow").apply {
|
||||
isAccessible = true
|
||||
invoke(this@dispatchDetachedForTest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BannerView.dispatchScrollStateForTest(state: Int) {
|
||||
val listenerField = BannerView::class.java.getDeclaredField("bannerScrollListener").apply { isAccessible = true }
|
||||
val listener = listenerField.get(this) as RecyclerView.OnScrollListener
|
||||
listener.onScrollStateChanged(findViewById(R.id.rv_banner), state)
|
||||
}
|
||||
|
||||
|
||||
private fun BannerView.setCurrentPositionForTest(position: Int) {
|
||||
BannerView::class.java.getDeclaredField("currentAdapterPosition").apply {
|
||||
isAccessible = true
|
||||
setInt(this@setCurrentPositionForTest, position)
|
||||
}
|
||||
BannerView::class.java.getDeclaredField("currentIndex").apply {
|
||||
isAccessible = true
|
||||
setInt(this@setCurrentPositionForTest, requireNotNull(findViewById<RecyclerView>(R.id.rv_banner).adapter as? BannerAdapter).toRealIndex(position))
|
||||
}
|
||||
}
|
||||
|
||||
private fun BannerView.currentAdapterPositionForTest(): Int {
|
||||
return BannerView::class.java.getDeclaredField("currentAdapterPosition").run {
|
||||
isAccessible = true
|
||||
getInt(this@currentAdapterPositionForTest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleItem(id: String) = BannerItem(
|
||||
bannerId = id,
|
||||
imageUrl = "https://example.com/banner-$id.png"
|
||||
|
||||
Reference in New Issue
Block a user