From e0df3af74f4593237b1955d20724e5640dd2c3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 23 Aug 2026 21:15:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(test):=20androidTest=20=EB=A5=BC=20?= =?UTF-8?q?=ED=98=84=ED=96=89=20=EB=8F=84=EB=A9=94=EC=9D=B8=20API=C2=B7?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B3=84=EC=95=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #861 로 들어온 androidTest 직후 배치 머지(#877→#867→#862→#864→#865)가 도메인 API 를 바꿨지만 androidTest 소스셋은 어떤 PR 검증도 컴파일하지 않아 컴파일 에러 44건이 develop 에 미검증 착륙했다(#936). 컴파일 44건 정리: - 수신자 홈 count API 제거(#865) → MindRecordReceiverRepository.getAll() + ReceiverTimeLetterRepository.getReceivedTimeLetters() 구조로 홈 테스트 재작성, 실패 섹션 문구("기록 수를 불러오지 못했습니다") 단언 포함 - author ListItem.id·홈 내비 콜백 String→Long - SettingViewModel.withdrawCompleted(Boolean) → WithdrawUiState 치환 - ReceivedAfternoteDetailRoute 의 onNavigateToPlaylist 필수화 반영 에뮬레이터 실측이 컴파일로는 안 잡히는 런타임 드리프트 7건을 추가 적발해 함께 수정: - SavedStateHandle 에 String id 주입 5건 — ViewModel 이 toRoute 로 Long 을 읽어 런타임 크래시. Long 주입으로 정정 - 마스터 키 입력이 UUID 형식 게이트(#887)에 서버 호출 전 반려 1건 — 유효 UUID 로 교체, 소문자 정규화 검증 겸함 - 탈퇴 확인 1건 — "완료 안내 후 확인 시 호출"이 "문장 일치 시 즉시 호출·결과 팝업" 구조로 바뀌어 시나리오 재작성 수정 6클래스 connectedDebugAndroidTest(Pixel_7_Claude_QA, API 35) 29/29 통과 실측. --- .../AfternoteAuthorExtendedAndroidTest.kt | 22 +-- .../AppAndReceiverCompletionAndroidTest.kt | 145 ++++++++++++------ .../ReceiverAdvancedAndroidTest.kt | 20 ++- .../SettingAccountSecurityAndroidTest.kt | 12 +- .../SettingCompletionAndroidTest.kt | 9 +- 5 files changed, 129 insertions(+), 79 deletions(-) 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..2b1cff22c8 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/AfternoteAuthorExtendedAndroidTest.kt @@ -120,9 +120,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 +157,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) { @@ -539,7 +539,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 +550,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 +602,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 975fb75c55..003e6830de 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt @@ -55,7 +55,6 @@ import com.afternote.feature.afternote.domain.error.ReceiverEmailAuthException import com.afternote.feature.afternote.domain.model.LeaveMessageBlock import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem import com.afternote.feature.afternote.domain.model.receiver.AfterNotesListResult -import com.afternote.feature.afternote.domain.model.receiver.LoadCountResult import com.afternote.feature.afternote.domain.model.receiver.ReceivedAfternoteDetail import com.afternote.feature.afternote.domain.model.receiver.ReceivedExportBundle import com.afternote.feature.afternote.domain.model.receiver.ReceivedPlaylistDetail @@ -70,6 +69,8 @@ import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfte import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailViewModel import com.afternote.feature.afternote.presentation.receiver.navigation.ReceiverNavActions import com.afternote.feature.afternote.presentation.receiver.navigation.model.ReceiverRoute +import com.afternote.feature.mindrecord.domain.model.ReceiverMindRecords +import com.afternote.feature.mindrecord.domain.repository.MindRecordReceiverRepository import com.afternote.feature.receiver.domain.model.DeliveryVerification import com.afternote.feature.receiver.domain.model.DeliveryVerificationStatus import com.afternote.feature.receiver.domain.model.ReceiverAuthPresignedUrl @@ -79,6 +80,9 @@ import com.afternote.feature.receiver.domain.model.SenderMessageInfo import com.afternote.feature.receiver.domain.repository.IdentityVerificationRepository import com.afternote.feature.receiver.domain.repository.ReceiverAuthRepository import com.afternote.feature.receiver.domain.repository.ReceiverDeliveryDocumentUploadRepository +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 @@ -228,10 +232,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 @@ -244,7 +265,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) } @@ -263,7 +284,7 @@ class ReceiverRuntimeCompletionAndroidTest { .onNodeWithText(context.getString(R.string.home_tab_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) } @@ -281,7 +302,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( @@ -297,7 +318,12 @@ class ReceiverRuntimeCompletionAndroidTest { .assertIsDisplayed() composeRule.onNodeWithText("언제나 응원할게").assertIsDisplayed() composeRule - .onNodeWithText("0개 마음의 기록이 있습니다.") + .onNodeWithText(context.getString(R.string.receiver_home_section_count_unavailable)) + .performScrollTo() + .assertIsDisplayed() + composeRule + .onNodeWithText("8개 라이프 이벤트 레터가 있습니다.") + .performScrollTo() .assertIsDisplayed() composeRule .onNodeWithText("2개의 애프터노트가 있습니다.") @@ -308,7 +334,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 @@ -622,14 +648,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) } } @@ -664,11 +690,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 { @@ -689,21 +715,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) @@ -713,33 +739,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 @@ -754,7 +786,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() } @@ -771,19 +803,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 7a90d4544d..69fed1dee1 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/ReceiverAdvancedAndroidTest.kt @@ -28,7 +28,6 @@ import com.afternote.feature.afternote.domain.AfternoteType import com.afternote.feature.afternote.domain.model.LeaveMessageBlock import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem import com.afternote.feature.afternote.domain.model.receiver.AfterNotesListResult -import com.afternote.feature.afternote.domain.model.receiver.LoadCountResult import com.afternote.feature.afternote.domain.model.receiver.ReceivedAccountCredentials import com.afternote.feature.afternote.domain.model.receiver.ReceivedAfternoteDetail import com.afternote.feature.afternote.domain.model.receiver.ReceivedExportBundle @@ -158,15 +157,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 +180,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 +189,7 @@ class ReceiverAdvancedAndroidTest { AfternoteTheme { ReceivedAfternoteDetailRoute( onBack = {}, + onNavigateToPlaylist = {}, viewModel = viewModel, ) } @@ -319,10 +321,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) From 549349852a1bc004faf7660174c745835cabbc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 23 Aug 2026 21:15:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci(unit-test):=20PR=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EC=97=90=20androidTest=20=EC=BB=B4=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=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 androidTest 소스셋은 unit-test.yml(JVM 테스트만)·GMD 스케줄(화·금, PR 트리거 없음) 어디서도 PR 시점에 컴파일되지 않아, main 코드 변경이 androidTest 를 깨뜨려도 머지 전 검출이 불가능했다(#936 실사고 — develop 컴파일 실패 44건). :app:compileDebugAndroidTestKotlin 을 기존 Gradle 한 방 실행에 추가한다. 에뮬레이터 없이 컴파일만이라 러너 비용은 소폭. required 승격은 #848 의 aggregate gate 와 연계 검토. --- .github/workflows/unit-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 50baf2a844..fe932d4dff 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 에 @@ -51,8 +54,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 + - name: Run unit tests, androidTest compile guard, and aggregate Kover reports + run: ./gradlew :koverXmlReportCi :koverHtmlReportCi :konsist:test :build-logic:test :app:compileDebugAndroidTestKotlin --continue --parallel - name: Upload Kover coverage reports id: kover-artifact