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>() {
 | 
			
		||||
 | 
			
		||||
    // 테스트/비연결 환경에서 notify* 호출로 인한 NPE 방지용 플래그
 | 
			
		||||
    private var isRecyclerViewAttached: Boolean = false
 | 
			
		||||
 | 
			
		||||
    interface Callback {
 | 
			
		||||
        fun onRetrySend(localId: String)
 | 
			
		||||
    }
 | 
			
		||||
@@ -122,7 +125,9 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 | 
			
		||||
        // 목록의 마지막에 추가
 | 
			
		||||
        items.add(ChatListItem.TypingIndicator)
 | 
			
		||||
        isTypingVisible = true
 | 
			
		||||
        notifyItemInserted(items.lastIndex)
 | 
			
		||||
        if (isRecyclerViewAttached) {
 | 
			
		||||
            notifyItemInserted(items.lastIndex)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -133,7 +138,9 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 | 
			
		||||
        if (index >= 0) {
 | 
			
		||||
            items.removeAt(index)
 | 
			
		||||
            isTypingVisible = false
 | 
			
		||||
            notifyItemRemoved(index)
 | 
			
		||||
            if (isRecyclerViewAttached) {
 | 
			
		||||
                notifyItemRemoved(index)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -480,6 +487,7 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 | 
			
		||||
        super.onAttachedToRecyclerView(recyclerView)
 | 
			
		||||
        // RecyclerView와 연결된 시점에 안정 ID 활성화 (JVM 테스트에서 NPE 회피)
 | 
			
		||||
        setHasStableIds(true)
 | 
			
		||||
        isRecyclerViewAttached = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
 | 
			
		||||
@@ -496,6 +504,11 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 | 
			
		||||
        super.onViewDetachedFromWindow(holder)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
 | 
			
		||||
        super.onDetachedFromRecyclerView(recyclerView)
 | 
			
		||||
        isRecyclerViewAttached = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
 | 
			
		||||
        when (holder) {
 | 
			
		||||
            is TypingIndicatorViewHolder -> holder.stopTypingAnimation()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,34 @@ import org.junit.Test
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    fun `getItemViewType returns correct types`() {
 | 
			
		||||
        val adapter = ChatMessageAdapter()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user