From 8def1512f3b79852df412ebaae9a4787f5370f2a Mon Sep 17 00:00:00 2001 From: seunghee Date: Thu, 20 Aug 2026 22:49:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=B1=EC=8B=A0=EB=90=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=ED=99=88=ED=99=94=EB=A9=B4=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamss/android/feature/home/HomeScreen.kt | 5 +++ .../android/feature/home/HomeViewModel.kt | 19 ++++++----- .../android/feature/home/HomeViewModelTest.kt | 33 +++++++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt index bc9d57e9..9b913ed6 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember @@ -70,6 +71,10 @@ fun HomeScreen( val state by viewModel.collectAsState() val context = LocalContext.current + // 닉네임 변경 화면에서 저장하고 돌아왔을 때 최신 정보를 다시 불러오기 위해 + // ViewModel init이 아니라 화면이 보일 때마다 실행되는 LaunchedEffect로 조회한다. + LaunchedEffect(Unit) { viewModel.loadUserInfo() } + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is HomeSideEffect.NavigateToSetting -> onNavigateToSetting() diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt index 39504024..9a812901 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt @@ -12,7 +12,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.blockingIntent -import org.orbitmvi.orbit.syntax.Syntax import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @@ -26,8 +25,16 @@ class HomeViewModel @Inject constructor( private val session: ConversationSession, ) : ViewModel(), ContainerHost { - // init 대신 onCreate 를 쓴다. 구독 시점에 한 번 돌고, 테스트에서 실행 시점을 잡을 수 있다. - override val container = container(HomeState()) { loadUserInfo() } + override val container = container(HomeState()) + + fun loadUserInfo() = intent { + // 닉네임을 못 받아도 화면은 성립한다. 세션이 끊긴 경우는 AuthRepository 가 로그인으로 되돌린다. + // 실패해도 기존 닉네임은 지우지 않는다 — 갱신 시도가 화면에 이미 보이던 값을 날리면 안 된다. + when (val result = getUserInfoUseCase()) { + is AppResult.Success -> reduce { state.copy(isLoading = false, nickname = result.data.nickname) } + is AppResult.Failure -> reduce { state.copy(isLoading = false) } + } + } fun navigateToSetting() = intent { postSideEffect(HomeSideEffect.NavigateToSetting) @@ -102,10 +109,4 @@ class HomeViewModel @Inject constructor( is AppResult.Failure -> postSideEffect(HomeSideEffect.ShowToast(SEND_FAILED)) } } - - private suspend fun Syntax.loadUserInfo() { - // 닉네임을 못 받아도 화면은 성립한다. 세션이 끊긴 경우는 AuthRepository 가 로그인으로 되돌린다. - val nickname = (getUserInfoUseCase() as? AppResult.Success)?.data?.nickname - reduce { state.copy(isLoading = false, nickname = nickname) } - } } diff --git a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt index e8ef53dd..a616438c 100644 --- a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt +++ b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt @@ -30,7 +30,7 @@ class HomeViewModelTest { givenUserInfo(nickname = "이소연") viewModel().test(this) { - runOnCreate() + containerHost.loadUserInfo() expectState { copy(isLoading = false, nickname = "이소연") } } } @@ -40,11 +40,40 @@ class HomeViewModelTest { coEvery { getUserInfoUseCase() } returns AppResult.Failure(IllegalStateException("boom")) viewModel().test(this) { - runOnCreate() + containerHost.loadUserInfo() expectState { copy(isLoading = false, nickname = null) } } } + // 닉네임 변경 화면에서 돌아오면 HomeScreen이 loadUserInfo를 다시 호출한다. 그 경로를 흉내낸다. + @Test + fun `닉네임 변경 후 돌아와 다시 불러오면 바뀐 값으로 갱신된다`() = runTest { + givenUserInfo(nickname = "이소연") + + viewModel().test(this) { + containerHost.loadUserInfo() + expectState { copy(isLoading = false, nickname = "이소연") } + + givenUserInfo(nickname = "소연이") + containerHost.loadUserInfo() + expectState { copy(nickname = "소연이") } + } + } + + @Test + fun `다시 불러오다 실패해도 이미 보이던 닉네임은 남는다`() = runTest { + givenUserInfo(nickname = "이소연") + + viewModel().test(this) { + containerHost.loadUserInfo() + expectState { copy(isLoading = false, nickname = "이소연") } + + coEvery { getUserInfoUseCase() } returns AppResult.Failure(IllegalStateException("boom")) + containerHost.loadUserInfo() + expectNoItems() + } + } + @Test fun `걱정을 적고 보내면 대화를 만들고 그 방을 연다`() = runTest { viewModel().test(this) { From 44542ae212e622aa53a70df37d5bfb8838e1804b Mon Sep 17 00:00:00 2001 From: seunghee Date: Fri, 21 Aug 2026 06:13:15 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=ED=99=88=20=EC=9D=B4=ED=83=88=20?= =?UTF-8?q?=ED=9B=84=20=EB=8C=80=ED=99=94=EB=B0=A9=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gamss/android/app/main/MainScreen.kt | 1 + .../gamss/android/feature/home/HomeScreen.kt | 19 ++++++++++-- .../android/feature/home/HomeSideEffect.kt | 5 +++- .../android/feature/home/HomeViewModel.kt | 29 +++++++++++++++++-- .../android/feature/home/HomeViewModelTest.kt | 21 ++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt b/app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt index 49fdc096..ec48c761 100644 --- a/app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt +++ b/app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt @@ -129,6 +129,7 @@ private fun mainEntryProvider(navigator: Navigator) = entryProvider { HomeScreen( onNavigateToSetting = { navigator.navigate(SettingKey) }, onOpenConversation = { conversationId -> navigator.navigate(ChatRoomKey(conversationId)) }, + isActive = navigator.state.currentKey == HomeKey, ) } entry { diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt index 9b913ed6..13259f43 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -65,20 +66,32 @@ import kotlin.math.roundToInt fun HomeScreen( onNavigateToSetting: () -> Unit, onOpenConversation: (Long) -> Unit, + isActive: Boolean = true, modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { val state by viewModel.collectAsState() val context = LocalContext.current - // 닉네임 변경 화면에서 저장하고 돌아왔을 때 최신 정보를 다시 불러오기 위해 - // ViewModel init이 아니라 화면이 보일 때마다 실행되는 LaunchedEffect로 조회한다. + LaunchedEffect(isActive) { + viewModel.onScreenActiveChanged(isActive) + } + + // 닉네임 변경 화면에서 저장하고 돌아왔을 때 최신 정보를 다시 불러온다. LaunchedEffect(Unit) { viewModel.loadUserInfo() } + DisposableEffect(Unit) { + onDispose { viewModel.onScreenActiveChanged(false) } + } + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is HomeSideEffect.NavigateToSetting -> onNavigateToSetting() - is HomeSideEffect.OpenConversation -> onOpenConversation(sideEffect.conversationId) + is HomeSideEffect.OpenConversation -> { + if (isActive && viewModel.shouldHandleOpenConversation(sideEffect.navigationGeneration)) { + onOpenConversation(sideEffect.conversationId) + } + } is HomeSideEffect.ShowToast -> Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() } diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt index 7483b02c..4598c751 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt @@ -4,7 +4,10 @@ sealed interface HomeSideEffect { data object NavigateToSetting : HomeSideEffect /** 대화는 이미 만들어졌다. 대화방은 이 id 로 조회만 한다. */ - data class OpenConversation(val conversationId: Long) : HomeSideEffect + data class OpenConversation( + val conversationId: Long, + val navigationGeneration: Long = 0L, + ) : HomeSideEffect data class ShowToast(val message: String) : HomeSideEffect } diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt index 9a812901..f4d14d2a 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt @@ -25,17 +25,31 @@ class HomeViewModel @Inject constructor( private val session: ConversationSession, ) : ViewModel(), ContainerHost { + // Top-level 화면은 back stack에 남아 ViewModel이 계속 살아 있을 수 있다. 화면을 벗어난 뒤 + // 이전 전송이 끝나도 자동 이동 이벤트가 되살아나지 않도록 화면 세대로 무효화한다. + private var isScreenActive = true + private var navigationGeneration = 0L + override val container = container(HomeState()) fun loadUserInfo() = intent { - // 닉네임을 못 받아도 화면은 성립한다. 세션이 끊긴 경우는 AuthRepository 가 로그인으로 되돌린다. - // 실패해도 기존 닉네임은 지우지 않는다 — 갱신 시도가 화면에 이미 보이던 값을 날리면 안 된다. + // 실패해도 기존 닉네임은 지우지 않는다. 갱신 시도가 화면에 이미 보이던 값을 날리면 안 된다. when (val result = getUserInfoUseCase()) { is AppResult.Success -> reduce { state.copy(isLoading = false, nickname = result.data.nickname) } is AppResult.Failure -> reduce { state.copy(isLoading = false) } } } + fun onScreenActiveChanged(active: Boolean) { + if (isScreenActive && !active) { + navigationGeneration++ + } + isScreenActive = active + } + + fun shouldHandleOpenConversation(generation: Long): Boolean = + isScreenActive && navigationGeneration == generation + fun navigateToSetting() = intent { postSideEffect(HomeSideEffect.NavigateToSetting) } @@ -88,6 +102,7 @@ class HomeViewModel @Inject constructor( if (pending == null) state else state.copy(isSending = true, isEmotionPickerExpanded = false) } val message = pending ?: return@intent + val requestGeneration = navigationGeneration val result = session.send( conversationId = null, @@ -103,10 +118,18 @@ class HomeViewModel @Inject constructor( // applicationScope 로 돌려서 이 화면을 벗어나도 끊기지 않는다. viewModelScope.launch { session.finishSend() } reduce { state.copy(input = "") } - postSideEffect(HomeSideEffect.OpenConversation(result.data.message.conversationId)) + if (isScreenActive && navigationGeneration == requestGeneration) { + postSideEffect( + HomeSideEffect.OpenConversation( + conversationId = result.data.message.conversationId, + navigationGeneration = requestGeneration, + ), + ) + } } // 입력은 남겨 둔다. 실패한 문구를 다시 치게 하면 안 된다. is AppResult.Failure -> postSideEffect(HomeSideEffect.ShowToast(SEND_FAILED)) } } + } diff --git a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt index a616438c..fd03537e 100644 --- a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt +++ b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt @@ -311,6 +311,27 @@ class HomeViewModelTest { assertEquals(1, gated.sendCount) } + @Test + fun `홈을 벗어난 뒤 전송이 끝나면 대화방 이동 이벤트를 발행하지 않는다`() = runTest { + val gate = CompletableDeferred() + val gated = RecordingConversationRepository(gate = gate) + + viewModel(gated).test(this) { + containerHost.onInputChange(WORRY) + expectState { copy(input = WORRY) } + + containerHost.onScreenActiveChanged(false) + containerHost.onSubmit() + expectState { copy(isSending = true) } + expectNoItems() + + gate.complete(Unit) + expectState { copy(isSending = false) } + expectState { copy(input = "") } + expectNoItems() + } + } + @Test fun `설정 아이콘을 누르면 설정으로 이동한다`() = runTest { viewModel().test(this) { From 8283f1c47fd265b7adf0884a6a13c2fcb83c54c1 Mon Sep 17 00:00:00 2001 From: seunghee Date: Fri, 21 Aug 2026 20:48:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=ED=99=88=20=EC=9D=B4=ED=83=88=20?= =?UTF-8?q?=EC=8B=9C=20=EB=8C=80=ED=99=94=EB=B0=A9=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=AC=B4?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamss/android/feature/home/HomeScreen.kt | 17 +++----- .../android/feature/home/HomeSideEffect.kt | 6 --- .../android/feature/home/HomeViewModel.kt | 29 +++---------- .../android/feature/home/HomeViewModelTest.kt | 43 ++++++++++++++----- 4 files changed, 43 insertions(+), 52 deletions(-) diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt index 13259f43..fd11108e 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -73,25 +72,19 @@ fun HomeScreen( val state by viewModel.collectAsState() val context = LocalContext.current - LaunchedEffect(isActive) { - viewModel.onScreenActiveChanged(isActive) - } - // 닉네임 변경 화면에서 저장하고 돌아왔을 때 최신 정보를 다시 불러온다. LaunchedEffect(Unit) { viewModel.loadUserInfo() } - DisposableEffect(Unit) { - onDispose { viewModel.onScreenActiveChanged(false) } + LaunchedEffect(isActive) { + if (!isActive) return@LaunchedEffect + viewModel.openConversationEvents.collect { conversationId -> + onOpenConversation(conversationId) + } } viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is HomeSideEffect.NavigateToSetting -> onNavigateToSetting() - is HomeSideEffect.OpenConversation -> { - if (isActive && viewModel.shouldHandleOpenConversation(sideEffect.navigationGeneration)) { - onOpenConversation(sideEffect.conversationId) - } - } is HomeSideEffect.ShowToast -> Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() } diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt index 4598c751..e5b4faf5 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeSideEffect.kt @@ -3,11 +3,5 @@ package com.gamss.android.feature.home sealed interface HomeSideEffect { data object NavigateToSetting : HomeSideEffect - /** 대화는 이미 만들어졌다. 대화방은 이 id 로 조회만 한다. */ - data class OpenConversation( - val conversationId: Long, - val navigationGeneration: Long = 0L, - ) : HomeSideEffect - data class ShowToast(val message: String) : HomeSideEffect } diff --git a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt index f4d14d2a..a606ead0 100644 --- a/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/gamss/android/feature/home/HomeViewModel.kt @@ -9,6 +9,8 @@ import com.gamss.android.domain.conversation.takeWithinMessageLimit import com.gamss.android.domain.emotion.EmotionCharacter import com.gamss.android.domain.user.GetUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.blockingIntent @@ -25,10 +27,8 @@ class HomeViewModel @Inject constructor( private val session: ConversationSession, ) : ViewModel(), ContainerHost { - // Top-level 화면은 back stack에 남아 ViewModel이 계속 살아 있을 수 있다. 화면을 벗어난 뒤 - // 이전 전송이 끝나도 자동 이동 이벤트가 되살아나지 않도록 화면 세대로 무효화한다. - private var isScreenActive = true - private var navigationGeneration = 0L + private val _openConversationEvents = MutableSharedFlow(replay = 0) + val openConversationEvents = _openConversationEvents.asSharedFlow() override val container = container(HomeState()) @@ -40,16 +40,6 @@ class HomeViewModel @Inject constructor( } } - fun onScreenActiveChanged(active: Boolean) { - if (isScreenActive && !active) { - navigationGeneration++ - } - isScreenActive = active - } - - fun shouldHandleOpenConversation(generation: Long): Boolean = - isScreenActive && navigationGeneration == generation - fun navigateToSetting() = intent { postSideEffect(HomeSideEffect.NavigateToSetting) } @@ -102,7 +92,6 @@ class HomeViewModel @Inject constructor( if (pending == null) state else state.copy(isSending = true, isEmotionPickerExpanded = false) } val message = pending ?: return@intent - val requestGeneration = navigationGeneration val result = session.send( conversationId = null, @@ -118,18 +107,10 @@ class HomeViewModel @Inject constructor( // applicationScope 로 돌려서 이 화면을 벗어나도 끊기지 않는다. viewModelScope.launch { session.finishSend() } reduce { state.copy(input = "") } - if (isScreenActive && navigationGeneration == requestGeneration) { - postSideEffect( - HomeSideEffect.OpenConversation( - conversationId = result.data.message.conversationId, - navigationGeneration = requestGeneration, - ), - ) - } + _openConversationEvents.emit(result.data.message.conversationId) } // 입력은 남겨 둔다. 실패한 문구를 다시 치게 하면 안 된다. is AppResult.Failure -> postSideEffect(HomeSideEffect.ShowToast(SEND_FAILED)) } } - } diff --git a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt index fd03537e..423ef6dc 100644 --- a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt +++ b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt @@ -9,7 +9,10 @@ import com.gamss.android.domain.user.UserProfile import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -84,7 +87,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() } assertEquals(WORRY, repository.sentContent) assertNull(repository.sentConversationId) @@ -117,7 +120,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() } assertEquals(setOf(EmotionCharacter.SADNESS), repository.sentExcludeCharacters) } @@ -176,7 +179,7 @@ class HomeViewModelTest { expectState { copy(isSending = true, isEmotionPickerExpanded = false) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() } } @@ -232,7 +235,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() containerHost.onInputChange(filled + "나") expectState { copy(input = filled) } @@ -261,7 +264,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() containerHost.onSubmit() expectNoItems() @@ -277,7 +280,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() containerHost.onInputChange(SECOND_WORRY) expectState { copy(input = SECOND_WORRY) } @@ -285,7 +288,7 @@ class HomeViewModelTest { expectState { copy(isSending = true) } expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() } assertEquals(listOf(null, null), repository.sentContextSummaries) } @@ -306,13 +309,34 @@ class HomeViewModelTest { gate.complete(Unit) expectState { copy(isSending = false) } expectState { copy(input = "") } - expectSideEffect(HomeSideEffect.OpenConversation(NEW_ROOM_ID)) + expectNoItems() } assertEquals(1, gated.sendCount) } @Test - fun `홈을 벗어난 뒤 전송이 끝나면 대화방 이동 이벤트를 발행하지 않는다`() = runTest { + fun `대화 생성이 완료되면 이동 이벤트를 발행한다`() = runTest { + val homeViewModel = viewModel() + val openConversation = async(start = CoroutineStart.UNDISPATCHED) { + homeViewModel.openConversationEvents.first() + } + + homeViewModel.test(this) { + containerHost.onInputChange(WORRY) + expectState { copy(input = WORRY) } + + containerHost.onSubmit() + expectState { copy(isSending = true) } + expectState { copy(isSending = false) } + expectState { copy(input = "") } + expectNoItems() + } + + assertEquals(NEW_ROOM_ID, openConversation.await()) + } + + @Test + fun `이동 이벤트를 수집하는 화면이 없으면 대화방 이동이 재생되지 않는다`() = runTest { val gate = CompletableDeferred() val gated = RecordingConversationRepository(gate = gate) @@ -320,7 +344,6 @@ class HomeViewModelTest { containerHost.onInputChange(WORRY) expectState { copy(input = WORRY) } - containerHost.onScreenActiveChanged(false) containerHost.onSubmit() expectState { copy(isSending = true) } expectNoItems() From bbd3a91d3fac3316e915f15e266535653101acbb Mon Sep 17 00:00:00 2001 From: seunghee Date: Fri, 21 Aug 2026 22:20:50 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20HomeViewModelTest.kt=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=ED=99=88=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=EC=8B=9C=20=EB=8C=80=ED=99=94=EB=B0=A9=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20-=20=ED=99=88=20=EC=9D=B4=ED=83=88=20=ED=9B=84=20?= =?UTF-8?q?=EB=8C=80=ED=99=94=EB=B0=A9=20=EC=9D=B4=EB=8F=99=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=9E=AC=EC=83=9D=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/home/HomeViewModelTest.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt index 423ef6dc..0226134c 100644 --- a/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt +++ b/feature/home/src/test/java/com/gamss/android/feature/home/HomeViewModelTest.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeoutOrNull import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -79,7 +80,12 @@ class HomeViewModelTest { @Test fun `걱정을 적고 보내면 대화를 만들고 그 방을 연다`() = runTest { - viewModel().test(this) { + val homeViewModel = viewModel() + val openConversation = async(start = CoroutineStart.UNDISPATCHED) { + homeViewModel.openConversationEvents.first() + } + + homeViewModel.test(this) { containerHost.onInputChange(WORRY) expectState { copy(input = WORRY) } @@ -89,6 +95,7 @@ class HomeViewModelTest { expectState { copy(input = "") } expectNoItems() } + assertEquals(NEW_ROOM_ID, openConversation.await()) assertEquals(WORRY, repository.sentContent) assertNull(repository.sentConversationId) } @@ -339,8 +346,9 @@ class HomeViewModelTest { fun `이동 이벤트를 수집하는 화면이 없으면 대화방 이동이 재생되지 않는다`() = runTest { val gate = CompletableDeferred() val gated = RecordingConversationRepository(gate = gate) + val homeViewModel = viewModel(gated) - viewModel(gated).test(this) { + homeViewModel.test(this) { containerHost.onInputChange(WORRY) expectState { copy(input = WORRY) } @@ -353,6 +361,11 @@ class HomeViewModelTest { expectState { copy(input = "") } expectNoItems() } + + val replayedEvent = withTimeoutOrNull(1) { + homeViewModel.openConversationEvents.first() + } + assertNull(replayedEvent) } @Test