test(chat-room): 타이핑 인디케이터 표시/중복/숨김 테스트 추가
- showTypingIndicator 중복 호출 시 중복 삽입 방지 검증 - hideTypingIndicator 안전성 검증(표시되지 않은 경우도 안전) - NPE 회귀 방지 fix(adapter): RecyclerView 미부착 상태에서 notify 호출로 NPE 발생 방지
This commit is contained in:
@@ -35,6 +35,9 @@ sealed class ChatListItem {
|
|||||||
|
|
||||||
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
// 테스트/비연결 환경에서 notify* 호출로 인한 NPE 방지용 플래그
|
||||||
|
private var isRecyclerViewAttached: Boolean = false
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRetrySend(localId: String)
|
fun onRetrySend(localId: String)
|
||||||
}
|
}
|
||||||
@@ -122,8 +125,10 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
// 목록의 마지막에 추가
|
// 목록의 마지막에 추가
|
||||||
items.add(ChatListItem.TypingIndicator)
|
items.add(ChatListItem.TypingIndicator)
|
||||||
isTypingVisible = true
|
isTypingVisible = true
|
||||||
|
if (isRecyclerViewAttached) {
|
||||||
notifyItemInserted(items.lastIndex)
|
notifyItemInserted(items.lastIndex)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 타이핑 인디케이터를 숨긴다(제거).
|
* 타이핑 인디케이터를 숨긴다(제거).
|
||||||
@@ -133,9 +138,11 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
items.removeAt(index)
|
items.removeAt(index)
|
||||||
isTypingVisible = false
|
isTypingVisible = false
|
||||||
|
if (isRecyclerViewAttached) {
|
||||||
notifyItemRemoved(index)
|
notifyItemRemoved(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun setItems(newItems: List<ChatListItem>) {
|
fun setItems(newItems: List<ChatListItem>) {
|
||||||
@@ -480,6 +487,7 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
super.onAttachedToRecyclerView(recyclerView)
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
// RecyclerView와 연결된 시점에 안정 ID 활성화 (JVM 테스트에서 NPE 회피)
|
// RecyclerView와 연결된 시점에 안정 ID 활성화 (JVM 테스트에서 NPE 회피)
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
|
isRecyclerViewAttached = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||||
@@ -496,6 +504,11 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||||||
super.onViewDetachedFromWindow(holder)
|
super.onViewDetachedFromWindow(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
super.onDetachedFromRecyclerView(recyclerView)
|
||||||
|
isRecyclerViewAttached = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
when (holder) {
|
when (holder) {
|
||||||
is TypingIndicatorViewHolder -> holder.stopTypingAnimation()
|
is TypingIndicatorViewHolder -> holder.stopTypingAnimation()
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ import org.junit.Test
|
|||||||
|
|
||||||
class ChatMessageAdapterTest {
|
class ChatMessageAdapterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `typing indicator shows only once and hides correctly`() {
|
||||||
|
val adapter = ChatMessageAdapter()
|
||||||
|
// 초기 아이템 1개 추가
|
||||||
|
val base = listOf(
|
||||||
|
ChatListItem.AiMessage(ChatMessage(1, "hi", "", mine = false, createdAt = 1L))
|
||||||
|
)
|
||||||
|
adapter.setItemsForTest(base)
|
||||||
|
val initialCount = adapter.itemCount
|
||||||
|
|
||||||
|
// 1) 표시: 하나 추가되고 마지막 아이템이 TypingIndicator 여야 함
|
||||||
|
adapter.showTypingIndicator()
|
||||||
|
assertEquals(initialCount + 1, adapter.itemCount)
|
||||||
|
assertEquals(ChatMessageAdapter.VIEW_TYPE_TYPING_INDICATOR, adapter.getItemViewType(adapter.itemCount - 1))
|
||||||
|
|
||||||
|
// 2) 중복 표시 시 개수 변화 없음
|
||||||
|
adapter.showTypingIndicator()
|
||||||
|
assertEquals(initialCount + 1, adapter.itemCount)
|
||||||
|
|
||||||
|
// 3) 숨김: 다시 원래 개수로 돌아감
|
||||||
|
adapter.hideTypingIndicator()
|
||||||
|
assertEquals(initialCount, adapter.itemCount)
|
||||||
|
|
||||||
|
// 4) 숨길 것이 없을 때 호출해도 안전 (변화 없음)
|
||||||
|
adapter.hideTypingIndicator()
|
||||||
|
assertEquals(initialCount, adapter.itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getItemViewType returns correct types`() {
|
fun `getItemViewType returns correct types`() {
|
||||||
val adapter = ChatMessageAdapter()
|
val adapter = ChatMessageAdapter()
|
||||||
|
|||||||
Reference in New Issue
Block a user