feat(banner): 배너 preview 회귀 테스트를 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -16,6 +16,7 @@ class BannerAdapter(
|
|||||||
|
|
||||||
private val items = mutableListOf<BannerItem>()
|
private val items = mutableListOf<BannerItem>()
|
||||||
private var itemSizePx: Int = ViewGroup.LayoutParams.MATCH_PARENT
|
private var itemSizePx: Int = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
private var previewImageResId: Int = 0
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BannerViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BannerViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_banner, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_banner, parent, false)
|
||||||
@@ -32,14 +33,24 @@ class BannerAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun submitItems(items: List<BannerItem>) {
|
fun submitItems(items: List<BannerItem>) {
|
||||||
|
val previousItemCount = itemCount
|
||||||
this.items.clear()
|
this.items.clear()
|
||||||
this.items.addAll(items)
|
this.items.addAll(items)
|
||||||
notifyDataSetChanged()
|
val newItemCount = itemCount
|
||||||
|
when {
|
||||||
|
previousItemCount == 0 && newItemCount > 0 -> notifyItemRangeInserted(0, newItemCount)
|
||||||
|
previousItemCount > 0 && newItemCount == 0 -> notifyItemRangeRemoved(0, previousItemCount)
|
||||||
|
previousItemCount == newItemCount -> notifyItemRangeChanged(0, newItemCount)
|
||||||
|
else -> {
|
||||||
|
notifyItemRangeRemoved(0, previousItemCount)
|
||||||
|
notifyItemRangeInserted(0, newItemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setItemSizePx(itemSizePx: Int) {
|
fun setItemSizePx(itemSizePx: Int) {
|
||||||
this.itemSizePx = itemSizePx
|
this.itemSizePx = itemSizePx
|
||||||
notifyDataSetChanged()
|
notifyItemsChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnClickItem(listener: ((BannerItem) -> Unit)?) {
|
fun setOnClickItem(listener: ((BannerItem) -> Unit)?) {
|
||||||
@@ -50,6 +61,11 @@ class BannerAdapter(
|
|||||||
onBindImage = listener
|
onBindImage = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPreviewImageResource(resId: Int) {
|
||||||
|
previewImageResId = resId
|
||||||
|
notifyItemsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
fun toRealIndex(position: Int): Int = if (items.isEmpty()) 0 else position % items.size
|
fun toRealIndex(position: Int): Int = if (items.isEmpty()) 0 else position % items.size
|
||||||
|
|
||||||
fun startPosition(realIndex: Int): Int {
|
fun startPosition(realIndex: Int): Int {
|
||||||
@@ -74,6 +90,7 @@ class BannerAdapter(
|
|||||||
height = ViewGroup.LayoutParams.MATCH_PARENT
|
height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
}
|
}
|
||||||
setRadiusClipping(imageView)
|
setRadiusClipping(imageView)
|
||||||
|
if (previewImageResId != 0) imageView.setImageResource(previewImageResId)
|
||||||
onBindImage?.invoke(imageView, item)
|
onBindImage?.invoke(imageView, item)
|
||||||
itemView.setOnClickListener { onClickItem?.invoke(item) }
|
itemView.setOnClickListener { onClickItem?.invoke(item) }
|
||||||
}
|
}
|
||||||
@@ -88,6 +105,11 @@ class BannerAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyItemsChanged() {
|
||||||
|
val count = itemCount
|
||||||
|
if (count > 0) notifyItemRangeChanged(0, count)
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val VIRTUAL_ITEM_COUNT = Int.MAX_VALUE
|
const val VIRTUAL_ITEM_COUNT = Int.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class BannerView @JvmOverloads constructor(
|
|||||||
separatorText = findViewById(R.id.tv_banner_counter_separator)
|
separatorText = findViewById(R.id.tv_banner_counter_separator)
|
||||||
totalCountText = findViewById(R.id.tv_banner_total_count)
|
totalCountText = findViewById(R.id.tv_banner_total_count)
|
||||||
setUpRecyclerView()
|
setUpRecyclerView()
|
||||||
|
applyPreviewAttributes(attrs)
|
||||||
updateCounter()
|
updateCounter()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +115,29 @@ class BannerView @JvmOverloads constructor(
|
|||||||
adapter.setOnBindImage(listener)
|
adapter.setOnBindImage(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyPreviewAttributes(attrs: AttributeSet?) {
|
||||||
|
if (attrs == null) return
|
||||||
|
|
||||||
|
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerView)
|
||||||
|
try {
|
||||||
|
val previewItemCount = typedArray.getInt(R.styleable.BannerView_bannerPreviewItemCount, 0)
|
||||||
|
val previewCurrentIndex = typedArray.getInt(R.styleable.BannerView_bannerPreviewCurrentIndex, 0)
|
||||||
|
val previewImageResId = typedArray.getResourceId(R.styleable.BannerView_bannerPreviewImage, 0)
|
||||||
|
if (previewImageResId != 0) adapter.setPreviewImageResource(previewImageResId)
|
||||||
|
if (previewItemCount > 0) {
|
||||||
|
items = List(previewItemCount) { index ->
|
||||||
|
BannerItem(bannerId = "preview-$index", imageUrl = "")
|
||||||
|
}
|
||||||
|
currentIndex = BannerState.from(previewItemCount, previewCurrentIndex).currentIndex
|
||||||
|
visibility = VISIBLE
|
||||||
|
adapter.submitItems(items)
|
||||||
|
scrollToCurrentBanner()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setUpRecyclerView() {
|
private fun setUpRecyclerView() {
|
||||||
requireNotNull(recyclerView).apply {
|
requireNotNull(recyclerView).apply {
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.junit.Assert.assertSame
|
|||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.Shadows.shadowOf
|
import org.robolectric.Shadows.shadowOf
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
@@ -100,6 +101,41 @@ class BannerViewTest {
|
|||||||
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_total_count).text.toString())
|
assertEquals("02", view.findViewById<TextView>(R.id.tv_banner_total_count).text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `배너 view는 preview 속성의 count와 current index를 counter에 반영한다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val attrs = Robolectric.buildAttributeSet()
|
||||||
|
.addAttribute(R.attr.bannerPreviewItemCount, "5")
|
||||||
|
.addAttribute(R.attr.bannerPreviewCurrentIndex, "2")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val view = BannerView(context, attrs)
|
||||||
|
|
||||||
|
assertEquals(View.VISIBLE, view.visibility)
|
||||||
|
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.layout_banner_counter).visibility)
|
||||||
|
assertEquals("03", view.findViewById<TextView>(R.id.tv_banner_current_index).text.toString())
|
||||||
|
assertEquals("05", view.findViewById<TextView>(R.id.tv_banner_total_count).text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `배너 view는 preview image 속성을 image view에 적용한다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val attrs = Robolectric.buildAttributeSet()
|
||||||
|
.addAttribute(R.attr.bannerPreviewItemCount, "1")
|
||||||
|
.addAttribute(R.attr.bannerPreviewImage, "@drawable/bg_banner_preview_placeholder")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val view = BannerView(context, attrs)
|
||||||
|
view.measure(exactly(402.dpToPx()), exactly(402.dpToPx()))
|
||||||
|
view.layout(0, 0, 402.dpToPx(), 402.dpToPx())
|
||||||
|
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_banner)
|
||||||
|
val holder = recyclerView.findViewHolderForAdapterPosition(0)
|
||||||
|
?: recyclerView.adapter!!.onCreateViewHolder(recyclerView, 0).also { recyclerView.adapter!!.onBindViewHolder(it, 0) }
|
||||||
|
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_banner)
|
||||||
|
|
||||||
|
assertNotNull(imageView.drawable)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `배너 view는 정사각형 item 크기와 좌우 padding 및 간격을 적용한다`() {
|
fun `배너 view는 정사각형 item 크기와 좌우 padding 및 간격을 적용한다`() {
|
||||||
val view = inflateBannerView()
|
val view = inflateBannerView()
|
||||||
@@ -166,6 +202,34 @@ class BannerViewTest {
|
|||||||
assertEquals(0, itemOffset.right)
|
assertEquals(0, itemOffset.right)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `배너 item은 click listener가 null이어도 클릭으로 crash가 발생하지 않는다`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val adapter = BannerAdapter()
|
||||||
|
adapter.submitItems(listOf(sampleItem("1")))
|
||||||
|
adapter.setOnClickItem(null)
|
||||||
|
val holder = adapter.onCreateViewHolder(FrameLayout(context), 0)
|
||||||
|
adapter.onBindViewHolder(holder, 0)
|
||||||
|
|
||||||
|
assertTrue(holder.itemView.performClick())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `배너 view는 첫 번째 배너 클릭 시 첫 번째 item을 callback으로 전달한다`() {
|
||||||
|
val view = inflateBannerView()
|
||||||
|
val firstItem = sampleItem("1")
|
||||||
|
var clickedItem: BannerItem? = null
|
||||||
|
|
||||||
|
view.setOnBannerClickListener { clickedItem = it }
|
||||||
|
view.setItems(listOf(firstItem))
|
||||||
|
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_banner)
|
||||||
|
val holder = recyclerView.adapter!!.onCreateViewHolder(recyclerView, 0)
|
||||||
|
recyclerView.adapter!!.onBindViewHolder(holder, 0)
|
||||||
|
holder.itemView.performClick()
|
||||||
|
|
||||||
|
assertEquals(firstItem, clickedItem)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `배너 item image는 radius clipping 대상으로 설정된다`() {
|
fun `배너 item image는 radius clipping 대상으로 설정된다`() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
|||||||
Reference in New Issue
Block a user