From 02747c539b8bddc2ae89e200c479a428f5f34df2 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 14 Aug 2025 19:19:38 +0900 Subject: [PATCH] =?UTF-8?q?test(chat-room):=20=ED=83=80=EC=9D=B4=ED=95=91?= =?UTF-8?q?=20=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C/=EC=A4=91=EB=B3=B5/=EC=88=A8=EA=B9=80=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - showTypingIndicator 중복 호출 시 중복 삽입 방지 검증 - hideTypingIndicator 안전성 검증(표시되지 않은 경우도 안전) - NPE 회귀 방지 fix(adapter): RecyclerView 미부착 상태에서 notify 호출로 NPE 발생 방지 --- .../chat/talk/room/ChatMessageAdapter.kt | 17 +++++++++-- .../chat/talk/room/ChatMessageAdapterTest.kt | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt index 5864201e..9706e116 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt @@ -35,6 +35,9 @@ sealed class ChatListItem { class ChatMessageAdapter : RecyclerView.Adapter() { + // 테스트/비연결 환경에서 notify* 호출로 인한 NPE 방지용 플래그 + private var isRecyclerViewAttached: Boolean = false + interface Callback { fun onRetrySend(localId: String) } @@ -122,7 +125,9 @@ class ChatMessageAdapter : RecyclerView.Adapter() { // 목록의 마지막에 추가 items.add(ChatListItem.TypingIndicator) isTypingVisible = true - notifyItemInserted(items.lastIndex) + if (isRecyclerViewAttached) { + notifyItemInserted(items.lastIndex) + } } /** @@ -133,7 +138,9 @@ class ChatMessageAdapter : RecyclerView.Adapter() { if (index >= 0) { items.removeAt(index) isTypingVisible = false - notifyItemRemoved(index) + if (isRecyclerViewAttached) { + notifyItemRemoved(index) + } } } @@ -480,6 +487,7 @@ class ChatMessageAdapter : RecyclerView.Adapter() { 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() { 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() diff --git a/app/src/test/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapterTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapterTest.kt index 2fed445b..7b757447 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapterTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapterTest.kt @@ -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()