feat(chat-ui): 메시지 그룹화, 시간 포맷팅, Repository 테스트 추가

This commit is contained in:
2025-08-14 18:08:01 +09:00
parent ec60d4f143
commit d662bd0b65
5 changed files with 207 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.chat.talk.room
import org.junit.Assert.assertEquals
import org.junit.Test
class ChatMessageAdapterTest {
@Test
fun `getItemViewType returns correct types`() {
val adapter = ChatMessageAdapter()
val list = listOf(
ChatListItem.UserMessage(ChatMessage(1, "hi", "", mine = true, createdAt = 1L)),
ChatListItem.AiMessage(ChatMessage(2, "hello", "", mine = false, createdAt = 2L)),
ChatListItem.Notice("notice"),
ChatListItem.TypingIndicator
)
adapter.setItemsForTest(list)
assertEquals(ChatMessageAdapter.VIEW_TYPE_USER_MESSAGE, adapter.getItemViewType(0))
assertEquals(ChatMessageAdapter.VIEW_TYPE_AI_MESSAGE, adapter.getItemViewType(1))
assertEquals(ChatMessageAdapter.VIEW_TYPE_NOTICE, adapter.getItemViewType(2))
assertEquals(ChatMessageAdapter.VIEW_TYPE_TYPING_INDICATOR, adapter.getItemViewType(3))
}
@Test
fun `computeGroupingFlags determines group and last correctly`() {
val items = listOf(
ChatListItem.AiMessage(ChatMessage(1, "a1", "", mine = false, createdAt = 1L)),
ChatListItem.AiMessage(ChatMessage(2, "a2", "", mine = false, createdAt = 2L)),
ChatListItem.AiMessage(ChatMessage(3, "a3", "", mine = false, createdAt = 3L)),
ChatListItem.UserMessage(ChatMessage(4, "u1", "", mine = true, createdAt = 4L)),
ChatListItem.UserMessage(ChatMessage(5, "u2", "", mine = true, createdAt = 5L)),
ChatListItem.Notice("guide"),
ChatListItem.UserMessage(ChatMessage(6, "u3", "", mine = true, createdAt = 6L)),
ChatListItem.AiMessage(ChatMessage(7, "a4", "", mine = false, createdAt = 7L))
)
// index 0: 첫 메시지, 그룹 아님, 다음과 같으므로 마지막 아님
ChatMessageAdapter.computeGroupingFlags(items, 0).let { (grouped, last) ->
assertEquals(false, grouped)
assertEquals(false, last)
}
// index 1: 이전과 동일 발신자 -> 그룹, 다음과 동일 -> 마지막 아님
ChatMessageAdapter.computeGroupingFlags(items, 1).let { (grouped, last) ->
assertEquals(true, grouped)
assertEquals(false, last)
}
// index 2: 이전과 동일 발신자 -> 그룹, 다음은 다른 발신자 -> 마지막
ChatMessageAdapter.computeGroupingFlags(items, 2).let { (grouped, last) ->
assertEquals(true, grouped)
assertEquals(true, last)
}
// index 3: 사용자 메시지 시작 -> 그룹 아님, 다음 동일 -> 마지막 아님
ChatMessageAdapter.computeGroupingFlags(items, 3).let { (grouped, last) ->
assertEquals(false, grouped)
assertEquals(false, last)
}
// index 4: 사용자 그룹의 마지막 (다음 아이템은 Notice라 발신자 비교 실패) -> grouped true, last true
ChatMessageAdapter.computeGroupingFlags(items, 4).let { (grouped, last) ->
assertEquals(true, grouped)
assertEquals(true, last)
}
// index 5: Notice -> 그룹 로직 비대상, grouped false, last true(다음과 비교 불가로 true)
ChatMessageAdapter.computeGroupingFlags(items, 5).let { (grouped, last) ->
assertEquals(false, grouped)
assertEquals(true, last)
}
// index 6: Notice 다음의 사용자 단독 -> grouped false, 다음은 ai라 last true
ChatMessageAdapter.computeGroupingFlags(items, 6).let { (grouped, last) ->
assertEquals(false, grouped)
assertEquals(true, last)
}
// index 7: 마지막 ai -> grouped false, last true
ChatMessageAdapter.computeGroupingFlags(items, 7).let { (grouped, last) ->
assertEquals(false, grouped)
assertEquals(true, last)
}
}
}

View File

@@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.chat.talk.room
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.chat.talk.TalkApi
import kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageDao
import kr.co.vividnext.sodalive.common.ApiResponse
import org.junit.Assert.assertEquals
import org.junit.Test
class ChatRepositoryTest {
@Test
fun `enterChatRoom inserts messages and returns response`() {
val dao = mockk<ChatMessageDao>(relaxed = true)
val api = mockk<TalkApi>()
val repo = ChatRepository(dao, api)
val serverMessages = listOf(
ServerChatMessage(1, "a1", "", mine = false, createdAt = 1000L),
ServerChatMessage(2, "u1", "", mine = true, createdAt = 2000L)
)
val character = CharacterInfo(10, "name", "", kr.co.vividnext.sodalive.chat.character.detail.CharacterType.CLONE)
val resp = ChatRoomEnterResponse(99, character, serverMessages, hasMoreMessages = false)
every { api.enterChatRoom(any(), any()) } returns Single.just(ApiResponse(true, resp, null))
coEvery { dao.getNthLatestCreatedAt(any(), any()) } returns null
val result = repo.enterChatRoom("token", 99).blockingGet()
// 반환 검증
assertEquals(99, result.roomId)
assertEquals(2, result.messages.size)
// 로컬 저장 검증
coVerify { dao.insertMessages(match { it.size == 2 }) }
}
@Test
fun `getRecentMessagesFromLocal maps to domain`() {
val dao = mockk<ChatMessageDao>()
val api = mockk<TalkApi>()
val repo = ChatRepository(dao, api)
val entities = listOf(
kr.co.vividnext.sodalive.chat.talk.room.db.ChatMessageEntity(
messageId = 1L,
roomId = 1L,
message = "hello",
profileImageUrl = "",
mine = true,
createdAt = 10L,
status = MessageStatus.SENT,
localId = null
)
)
coEvery { dao.getRecentMessages(1L) } returns entities
val list = repo.getRecentMessagesFromLocal(1L).blockingGet()
assertEquals(1, list.size)
assertEquals("hello", list[0].message)
assertEquals(true, list[0].mine)
}
}

View File

@@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.chat.talk.room
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.Locale
class TimeUtilsTest {
@Test
fun `formatMessageTime returns localized pattern for ko_KR`() {
val ts = 1734190980000L // 임의 epoch millis
val result = formatMessageTime(ts, Locale.KOREA)
// 한국어 로케일일 때 "오전/오후 h:mm" 패턴 일부라도 충족하는지 확인
assertTrue(result.isNotBlank())
assertTrue(result.contains(":"))
// 오전 또는 오후 텍스트 포함
assertTrue(result.contains("오전") || result.contains("오후"))
}
}