diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f3a2e18567..523882580f 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,6 +5,9 @@ name: Unit Test # - :koverXmlReportCi / :koverHtmlReportCi : Android debug + 순수 JVM 테스트와 aggregate coverage # - :konsist:test : 순수 JVM 모듈(testDebugUnitTest 미보유)이라 별도 명시. 레이어 의존 아키텍처 가드. # - :build-logic:test : included build 라 루트 태스크에 안 묶여 별도 명시. release 키 가드(TestKit) 회귀 가드. +# - :app:compileDebugAndroidTestKotlin : androidTest 소스셋 컴파일 가드. GMD 스위트는 PR 트리거 없이 +# 화·금 스케줄로만 돌아(android-managed-device.yml), main 코드 변경이 androidTest 를 깨뜨려도 +# 머지 전 어디서도 검출되지 않았다(#936 — develop 컴파일 실패 44건). 에뮬레이터 없이 컴파일만 검증. # lint.yml 과 setup 이 거의 동일하지만 분리 — lint 실패가 test 결과를 가리지 않도록 fail-fast 독립. # base 필터를 두지 않는다 — base 변경(feat/* → develop)은 pull_request 기본 activity type 에 @@ -55,8 +58,8 @@ jobs: - name: Set up CI-only app configuration uses: ./.github/actions/setup-ci-config - - name: Run unit tests and aggregate Kover reports - run: ./gradlew :koverXmlReportCi :koverHtmlReportCi :konsist:test :build-logic:test --continue --parallel --build-cache + - name: Run unit tests, androidTest compile guard, and aggregate Kover reports + run: ./gradlew :koverXmlReportCi :koverHtmlReportCi :konsist:test :build-logic:test :app:compileDebugAndroidTestKotlin --continue --parallel --build-cache - name: Upload Kover coverage reports id: kover-artifact diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt index 147f4dcbb9..57cbee8eef 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt @@ -93,6 +93,7 @@ import org.junit.Test import org.junit.runner.RunWith import java.time.LocalDate import java.util.concurrent.atomic.AtomicInteger +import com.afternote.feature.afternote.presentation.R as AfternoteFeatureR @RunWith(AndroidJUnit4::class) class AfternoteAuthorExtendedAndroidTest { @@ -120,9 +121,9 @@ class AfternoteAuthorExtendedAndroidTest { Pager(PagingConfig(pageSize = 20)) { pagingSource }.flow repository.listFlows[AfternoteType.SOCIAL_NETWORK] = flowOf(PagingData.empty()) val viewModel = AfternoteHomeViewModel(repository) - val accountRoutes = mutableListOf() - val galleryRoutes = mutableListOf() - val memorialRoutes = mutableListOf() + val accountRoutes = mutableListOf() + val galleryRoutes = mutableListOf() + val memorialRoutes = mutableListOf() val addRoutes = mutableListOf() composeRule.setContent { @@ -157,9 +158,9 @@ class AfternoteAuthorExtendedAndroidTest { .onNodeWithContentDescription("추억 노트") .performScrollTo() .performClick() - assertEquals(listOf("101"), accountRoutes) - assertEquals(listOf("102"), galleryRoutes) - assertEquals(listOf("103"), memorialRoutes) + assertEquals(listOf(101L), accountRoutes) + assertEquals(listOf(102L), galleryRoutes) + assertEquals(listOf(103L), memorialRoutes) composeRule.onNodeWithText("소셜네트워크").performClick() composeRule.waitUntil(timeoutMillis = 5_000) { @@ -413,7 +414,7 @@ private fun AuthorDetailForDelete( is AfternoteDetailDeleteResult.Failed -> { val message = resources.getString( - result.messageRes ?: R.string.afternote_detail_delete_failed, + result.messageRes ?: AfternoteFeatureR.string.afternote_detail_delete_failed, ) scope.launch { snackbarHostState.showSnackbar(message) } viewModel.onDeleteResultConsumed() @@ -539,7 +540,7 @@ private fun detailViewModel( itemId: Long, ): AfternoteDetailViewModel = AfternoteDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf("itemId" to itemId.toString())), + savedStateHandle = SavedStateHandle(mapOf("itemId" to itemId)), afternoteRepository = repository, userRepository = FakeUserRepository(), errorReporter = FakeErrorReporter(), @@ -550,7 +551,7 @@ private fun editorViewModel( itemId: Long, ): AfternoteEditorViewModel = AfternoteEditorViewModel( - savedStateHandle = SavedStateHandle(mapOf("itemId" to itemId.toString())), + savedStateHandle = SavedStateHandle(mapOf("itemId" to itemId)), authorReceiverRepository = ExtendedAuthorReceiverRepository(), afternoteRepository = repository, memorialThumbnailUploadRepository = @@ -602,19 +603,19 @@ private fun editorViewModel( private fun authorListItems(): List = listOf( ListItem( - id = "101", + id = 101L, serviceName = "Instagram", date = "2026.08.22", type = AfternoteType.SOCIAL_NETWORK, ), ListItem( - id = "102", + id = 102L, serviceName = "Google Drive", date = "2026.08.22", type = AfternoteType.GALLERY_AND_FILES, ), ListItem( - id = "103", + id = 103L, serviceName = "추억 노트", date = "2026.08.22", type = AfternoteType.MEMORIAL, diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt index 79ca19b21c..c3b76d795e 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt @@ -47,12 +47,13 @@ import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.afternote.domain.AfternoteType import com.afternote.feature.afternote.domain.model.LeaveMessageBlock import com.afternote.feature.home.presentation.HomeTabActions +import com.afternote.feature.mindrecord.domain.model.ReceiverMindRecords +import com.afternote.feature.mindrecord.domain.repository.MindRecordReceiverRepository import com.afternote.feature.receiver.domain.error.ReceiverEmailAuthException import com.afternote.feature.receiver.domain.model.AfterNoteListItem import com.afternote.feature.receiver.domain.model.AfterNotesListResult import com.afternote.feature.receiver.domain.model.DeliveryVerification import com.afternote.feature.receiver.domain.model.DeliveryVerificationStatus -import com.afternote.feature.receiver.domain.model.LoadCountResult import com.afternote.feature.receiver.domain.model.ReceivedAfternoteDetail import com.afternote.feature.receiver.domain.model.ReceivedExportBundle import com.afternote.feature.receiver.domain.model.ReceivedPlaylistDetail @@ -79,6 +80,9 @@ import com.afternote.feature.receiver.presentation.home.ReceiverHomeViewModel import com.afternote.feature.receiver.presentation.home.model.ReceiverHomeUiState import com.afternote.feature.receiver.presentation.navigation.ReceiverNavActions import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRoute +import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetter +import com.afternote.feature.timeletter.domain.model.ReceivedTimeLetterList +import com.afternote.feature.timeletter.domain.repository.ReceiverTimeLetterRepository import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.CompletableDeferred @@ -230,10 +234,27 @@ class ReceiverRuntimeCompletionAndroidTest { @Test fun receiverHome_allFailureThenRetryPartialSuccess_keepsAvailableSectionsAndReportsBothStages() { val repository = CompletionReceiverRepository() - val allFailureAttempt = repository.enqueueHomeAttempt() - val partialAttempt = repository.enqueueHomeAttempt() + val mindRecordRepository = CompletionMindRecordReceiverRepository() + val timeLetterRepository = CompletionReceiverTimeLetterRepository() + + fun homeCallCounts(): List = + listOf( + repository.afterNotesCalls, + mindRecordRepository.getAllCalls, + timeLetterRepository.listCalls, + repository.senderMessageCalls, + ) + + val allFailureAttempt = enqueueHomeAttempt(repository, mindRecordRepository, timeLetterRepository) + val partialAttempt = enqueueHomeAttempt(repository, mindRecordRepository, timeLetterRepository) val reporter = FakeErrorReporter() - val viewModel = ReceiverHomeViewModel(repository, reporter) + val viewModel = + ReceiverHomeViewModel( + receiverRepository = repository, + mindRecordReceiverRepository = mindRecordRepository, + receiverTimeLetterRepository = timeLetterRepository, + errorReporter = reporter, + ) composeRule.setContent { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value @@ -246,7 +267,7 @@ class ReceiverRuntimeCompletionAndroidTest { } } - composeRule.waitUntil(timeoutMillis = 5_000) { repository.homeCallCounts.all { it == 1 } } + composeRule.waitUntil(timeoutMillis = 5_000) { homeCallCounts().all { it == 1 } } composeRule.runOnIdle { assertSame(ReceiverHomeUiState.Loading, viewModel.uiState.value) } @@ -265,7 +286,7 @@ class ReceiverRuntimeCompletionAndroidTest { .onNodeWithText(context.getString(ReceiverR.string.receiver_home_retry)) .performClick() - composeRule.waitUntil(timeoutMillis = 5_000) { repository.homeCallCounts.all { it == 2 } } + composeRule.waitUntil(timeoutMillis = 5_000) { homeCallCounts().all { it == 2 } } composeRule.runOnIdle { assertSame(ReceiverHomeUiState.Loading, viewModel.uiState.value) } @@ -283,7 +304,7 @@ class ReceiverRuntimeCompletionAndroidTest { ), ), mindRecords = Result.failure(IllegalStateException("mind records unavailable")), - timeLetters = Result.success(LoadCountResult(totalCount = 8)), + timeLetters = Result.success(ReceivedTimeLetterList(timeLetters = emptyList(), totalCount = 8)), senderMessage = Result.success( SenderMessageInfo( @@ -299,7 +320,12 @@ class ReceiverRuntimeCompletionAndroidTest { .assertIsDisplayed() composeRule.onNodeWithText("언제나 응원할게").assertIsDisplayed() composeRule - .onNodeWithText("0개 마음의 기록이 있습니다.") + .onNodeWithText(context.getString(ReceiverR.string.receiver_home_section_count_unavailable)) + .performScrollTo() + .assertIsDisplayed() + composeRule + .onNodeWithText("8개 라이프 이벤트 레터가 있습니다.") + .performScrollTo() .assertIsDisplayed() composeRule .onNodeWithText("2개의 애프터노트가 있습니다.") @@ -310,7 +336,7 @@ class ReceiverRuntimeCompletionAndroidTest { assertEquals("receiver_home_load", reporter.failures[0].second["home_stage"]) assertEquals("receiver_home_partial_load", reporter.failures[1].second["home_stage"]) assertEquals("mind_records", reporter.failures[1].second["home_failed_sources"]) - assertEquals(listOf(2, 2, 2, 2), repository.homeCallCounts) + assertEquals(listOf(2, 2, 2, 2), homeCallCounts()) } @Test @@ -624,14 +650,14 @@ class ReceiverRuntimeCompletionAndroidTest { ) val viewModel = ReceivedAfternoteDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf("afternoteId" to "202")), + savedStateHandle = SavedStateHandle(mapOf("afternoteId" to 202L)), receiverRepository = repository, errorReporter = FakeErrorReporter(), ) composeRule.setContent { AfternoteTheme { - ReceivedAfternoteDetailRoute(onBack = {}, viewModel = viewModel) + ReceivedAfternoteDetailRoute(onBack = {}, onNavigateToPlaylist = {}, viewModel = viewModel) } } @@ -666,11 +692,11 @@ class ReceiverRuntimeCompletionAndroidTest { ) val viewModel = ReceivedAfternoteDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf("afternoteId" to "303")), + savedStateHandle = SavedStateHandle(mapOf("afternoteId" to 303L)), receiverRepository = repository, errorReporter = FakeErrorReporter(), ) - val playlistRoutes = mutableListOf() + val playlistRoutes = mutableListOf() composeRule.setContent { AfternoteTheme { @@ -691,21 +717,21 @@ class ReceiverRuntimeCompletionAndroidTest { .onNode(hasText("추억 플레이리스트") and hasClickAction()) .performClick() - assertEquals(listOf("303"), playlistRoutes) + assertEquals(listOf(303L), playlistRoutes) assertEquals(listOf(303L), repository.detailIds) } } private data class PendingHomeAttempt( val afterNotes: CompletableDeferred>, - val mindRecords: CompletableDeferred>, - val timeLetters: CompletableDeferred>, + val mindRecords: CompletableDeferred>, + val timeLetters: CompletableDeferred>, val senderMessage: CompletableDeferred>, ) { fun complete( afterNotes: Result, - mindRecords: Result, - timeLetters: Result, + mindRecords: Result, + timeLetters: Result, senderMessage: Result, ) { this.afterNotes.complete(afterNotes) @@ -715,33 +741,39 @@ private data class PendingHomeAttempt( } } +/** 홈 한 번의 로드가 물리는 세 리포지토리 대기열에 결과 게이트를 한 벌씩 건다. */ +private fun enqueueHomeAttempt( + receiverRepository: CompletionReceiverRepository, + mindRecordRepository: CompletionMindRecordReceiverRepository, + timeLetterRepository: CompletionReceiverTimeLetterRepository, +): PendingHomeAttempt { + val attempt = + PendingHomeAttempt( + afterNotes = CompletableDeferred(), + mindRecords = CompletableDeferred(), + timeLetters = CompletableDeferred(), + senderMessage = CompletableDeferred(), + ) + receiverRepository.afterNoteHomeResults.addLast(attempt.afterNotes) + mindRecordRepository.homeResults.addLast(attempt.mindRecords) + timeLetterRepository.homeResults.addLast(attempt.timeLetters) + receiverRepository.senderMessageHomeResults.addLast(attempt.senderMessage) + return attempt +} + private class CompletionReceiverRepository : ReceiverRepository { private val authCode = MutableStateFlow(null) override val authCodeFlow: Flow = authCode - private val afterNoteHomeResults = ArrayDeque>>() - private val mindRecordHomeResults = ArrayDeque>>() - private val timeLetterHomeResults = ArrayDeque>>() - private val senderMessageHomeResults = ArrayDeque>>() + val afterNoteHomeResults = ArrayDeque>>() + val senderMessageHomeResults = ArrayDeque>>() val detailResults = ArrayDeque>() val detailIds = mutableListOf() - val homeCallCounts = mutableListOf(0, 0, 0, 0) - - fun enqueueHomeAttempt(): PendingHomeAttempt { - val attempt = - PendingHomeAttempt( - afterNotes = CompletableDeferred(), - mindRecords = CompletableDeferred(), - timeLetters = CompletableDeferred(), - senderMessage = CompletableDeferred(), - ) - afterNoteHomeResults.addLast(attempt.afterNotes) - mindRecordHomeResults.addLast(attempt.mindRecords) - timeLetterHomeResults.addLast(attempt.timeLetters) - senderMessageHomeResults.addLast(attempt.senderMessage) - return attempt - } + var afterNotesCalls = 0 + private set + var senderMessageCalls = 0 + private set override suspend fun currentAuthCode(): String? = authCode.value @@ -756,7 +788,7 @@ private class CompletionReceiverRepository : ReceiverRepository { override fun getPagedReceivedAfternotes(): Flow> = flowOf(PagingData.empty()) override suspend fun getReceivedAfterNotes(): Result { - homeCallCounts[0] += 1 + afterNotesCalls += 1 return afterNoteHomeResults.removeFirst().await() } @@ -773,19 +805,36 @@ private class CompletionReceiverRepository : ReceiverRepository { error("unexpected saveReceivedExportToFile") } - override suspend fun loadMindRecordsCount(): Result { - homeCallCounts[1] += 1 - return mindRecordHomeResults.removeFirst().await() + override suspend fun loadSenderMessage(): Result { + senderMessageCalls += 1 + return senderMessageHomeResults.removeFirst().await() } +} + +private class CompletionMindRecordReceiverRepository : MindRecordReceiverRepository { + val homeResults = ArrayDeque>>() + var getAllCalls = 0 + private set - override suspend fun loadTimeLettersCount(): Result { - homeCallCounts[2] += 1 - return timeLetterHomeResults.removeFirst().await() + override suspend fun getAll(): Result { + getAllCalls += 1 + return homeResults.removeFirst().await() } +} - override suspend fun loadSenderMessage(): Result { - homeCallCounts[3] += 1 - return senderMessageHomeResults.removeFirst().await() +private class CompletionReceiverTimeLetterRepository : ReceiverTimeLetterRepository { + val homeResults = ArrayDeque>>() + var listCalls = 0 + private set + + // 실패는 throw 로 전달 — 인터페이스가 Result 대신 예외 계약이라 ViewModel 쪽 runCatching 이 받는다. + override suspend fun getReceivedTimeLetters(): ReceivedTimeLetterList { + listCalls += 1 + return homeResults.removeFirst().await().getOrThrow() + } + + override suspend fun getReceivedTimeLetterDetail(timeLetterReceiverId: Long): ReceivedTimeLetter { + error("unexpected getReceivedTimeLetterDetail") } } diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt index 1312341d26..23893c724d 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt @@ -30,7 +30,6 @@ import com.afternote.feature.afternote.presentation.R import com.afternote.feature.receiver.domain.model.AfterNoteListItem import com.afternote.feature.receiver.domain.model.AfterNotesListResult import com.afternote.feature.receiver.domain.model.DeliveryVerification -import com.afternote.feature.receiver.domain.model.LoadCountResult import com.afternote.feature.receiver.domain.model.ReceivedAccountCredentials import com.afternote.feature.receiver.domain.model.ReceivedAfternoteDetail import com.afternote.feature.receiver.domain.model.ReceivedExportBundle @@ -62,6 +61,7 @@ import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import com.afternote.feature.afternote.presentation.R as AfternoteFeatureR @RunWith(AndroidJUnit4::class) class ReceiverAdvancedAndroidTest { @@ -158,15 +158,17 @@ class ReceiverAdvancedAndroidTest { .assertIsDisplayed() val nextButton = hasText("다음") and hasClickAction() composeRule.onNode(nextButton).assertIsNotEnabled() - composeRule.onNode(hasSetTextAction()).performTextInput(" MASTER-KEY-73 ") + // 마스터 키는 UUID 형식만 통과하고 소문자로 정규화된다(#887) — 공백·대문자 입력으로 함께 검증. + composeRule.onNode(hasSetTextAction()).performTextInput(" 3F2504E0-4F89-11D3-9A0C-0305E82C3301 ") composeRule.onNode(nextButton).assertIsEnabled().performClick() composeRule.waitUntil(timeoutMillis = 5_000) { verifiedTransitions == 1 } + val normalizedMasterKey = "3f2504e0-4f89-11d3-9a0c-0305e82c3301" val attached = checkNotNull(senderRegistry.findById(sender.id)) - assertEquals(listOf("MASTER-KEY-73"), authRepository.verifiedMasterKeys) - assertEquals(listOf("MASTER-KEY-73"), receiverRepository.savedAuthCodes) - assertEquals("MASTER-KEY-73", receiverRepository.authCode.value) - assertEquals("MASTER-KEY-73", attached.authCode) + assertEquals(listOf(normalizedMasterKey), authRepository.verifiedMasterKeys) + assertEquals(listOf(normalizedMasterKey), receiverRepository.savedAuthCodes) + assertEquals(normalizedMasterKey, receiverRepository.authCode.value) + assertEquals(normalizedMasterKey, attached.authCode) assertEquals("이발신", attached.realSenderName) assertEquals("가족", attached.relation) assertEquals(1, verifiedTransitions) @@ -179,7 +181,7 @@ class ReceiverAdvancedAndroidTest { repository.detailResults.addLast(Result.success(receivedSocialDetail())) val viewModel = ReceivedAfternoteDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf("afternoteId" to "91")), + savedStateHandle = SavedStateHandle(mapOf("afternoteId" to 91L)), receiverRepository = repository, errorReporter = FakeErrorReporter(), ) @@ -188,6 +190,7 @@ class ReceiverAdvancedAndroidTest { AfternoteTheme { ReceivedAfternoteDetailRoute( onBack = {}, + onNavigateToPlaylist = {}, viewModel = viewModel, ) } @@ -234,14 +237,14 @@ class ReceiverAdvancedAndroidTest { confirmDownload() composeRule.waitUntil(timeoutMillis = 5_000) { - viewModel.uiState.value.errorMessageRes == R.string.receiver_download_all_failed + viewModel.uiState.value.errorMessageRes == AfternoteFeatureR.string.receiver_download_all_failed } assertEquals(1, repository.downloadCalls) assertEquals(0, repository.savedBundles.size) confirmDownload() composeRule.waitUntil(timeoutMillis = 5_000) { - viewModel.uiState.value.errorMessageRes == R.string.receiver_download_all_save_failed + viewModel.uiState.value.errorMessageRes == AfternoteFeatureR.string.receiver_download_all_save_failed } assertEquals(2, repository.downloadCalls) assertEquals(listOf(saveFailureBundle), repository.savedBundles) @@ -319,10 +322,6 @@ private class AdvancedReceiverRepository : ReceiverRepository { return saveResults.removeFirst() } - override suspend fun loadMindRecordsCount(): Result = Result.success(LoadCountResult(totalCount = 0)) - - override suspend fun loadTimeLettersCount(): Result = Result.success(LoadCountResult(totalCount = 0)) - override suspend fun loadSenderMessage(): Result = Result.success(null) } diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/SettingAccountSecurityAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/SettingAccountSecurityAndroidTest.kt index 283c2ba685..00bf6b3fcb 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/SettingAccountSecurityAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/SettingAccountSecurityAndroidTest.kt @@ -46,6 +46,7 @@ import com.afternote.feature.setting.presentation.viewmodel.ProfileEditViewModel import com.afternote.feature.setting.presentation.viewmodel.ReceiverRegisterViewModel import com.afternote.feature.setting.presentation.viewmodel.SettingUiState import com.afternote.feature.setting.presentation.viewmodel.SettingViewModel +import com.afternote.feature.setting.presentation.viewmodel.WithdrawUiState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first @@ -341,15 +342,16 @@ class SettingAccountSecurityAndroidTest { composeRule.onNodeWithText("탈퇴하겠습니다").performTextInput("탈퇴하겠습니다") composeRule.onNodeWithText("탈퇴하기").performClick() - composeRule.onNodeWithText("회원 탈퇴가 완료되었습니다.\n애프터노트를 이용해 주셔서 감사합니다.").assertIsDisplayed() - assertEquals(0, userRepository.deleteAccountCalls) - - composeRule.onNodeWithText("확인하기").performClick() composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { userRepository.deleteAccountCalls == 1 } - assertFalse(viewModel.withdrawCompleted.value) + composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { + viewModel.withdrawUiState.value == WithdrawUiState.Error + } + composeRule + .onNodeWithText("회원 탈퇴에 실패했습니다. 잠시 후 다시 시도해 주세요.") + .assertIsDisplayed() assertEquals(0, successCalls) assertEquals(0, authRepository.clearSessionCalls) assertTrue(runBlocking { authRepository.isLoggedIn.first() }) diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/SettingCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/SettingCompletionAndroidTest.kt index f2a804caa7..db967a1b8d 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/SettingCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/SettingCompletionAndroidTest.kt @@ -56,6 +56,7 @@ import com.afternote.feature.setting.presentation.viewmodel.ReceiverRegisterEven import com.afternote.feature.setting.presentation.viewmodel.ReceiverRegisterViewModel import com.afternote.feature.setting.presentation.viewmodel.SettingUiState import com.afternote.feature.setting.presentation.viewmodel.SettingViewModel +import com.afternote.feature.setting.presentation.viewmodel.WithdrawUiState import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -514,9 +515,9 @@ class SettingCompletionAndroidTest { composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { userApi.completedDeleteCalls == 1 } - composeRule.waitForIdle() - - assertFalse(viewModel.withdrawCompleted.value) + composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { + viewModel.withdrawUiState.value == WithdrawUiState.Error + } assertEquals(0, authRepository.clearSessionCalls) assertTrue(runBlocking { authRepository.isLoggedIn.first() }) @@ -528,7 +529,7 @@ class SettingCompletionAndroidTest { retryGate.complete(BaseResponse(status = 200, code = 200)) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - viewModel.withdrawCompleted.value + viewModel.withdrawUiState.value == WithdrawUiState.Success } assertEquals(2, userApi.completedDeleteCalls)