From ea9988826d2223a216f646aa5bc0aefd673018b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 23 Aug 2026 21:15:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test(app):=20develop=20androidTest=20?= =?UTF-8?q?=EC=BB=B4=ED=8C=8C=EC=9D=BC=20=EB=B3=B5=EA=B5=AC=20=E2=80=94=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=EC=A1=B0=ED=95=A9=EC=9D=98=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98=20=EB=B3=80=EA=B2=BD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #861/#867 androidTest 배치가 develop 의 도메인 API 교체(#868 등)와 조합 컴파일 검증 없이 머지돼 :app:compileDebugAndroidTestKotlin 이 깨져 있던 것을 복구. ListItem/내비게이션 Long 화, ReceiverHomeViewModel 4-인자 시그니처와 분리 리포지토리 구조 반영, 삭제된 LoadCountResult·withdrawCompleted 참조를 현행 모델(ReceiverMindRecords/ReceivedTimeLetterList·WithdrawUiState)로 교체. --- .../AfternoteAuthorExtendedAndroidTest.kt | 18 ++--- .../AppAndReceiverCompletionAndroidTest.kt | 68 ++++++++++++------- .../ReceiverAdvancedAndroidTest.kt | 6 +- .../SettingAccountSecurityAndroidTest.kt | 10 ++- .../SettingCompletionAndroidTest.kt | 9 +-- 5 files changed, 64 insertions(+), 47 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..7eab893f24 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) { @@ -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..1ca45ad40c 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 @@ -231,7 +235,13 @@ class ReceiverRuntimeCompletionAndroidTest { val allFailureAttempt = repository.enqueueHomeAttempt() val partialAttempt = repository.enqueueHomeAttempt() val reporter = FakeErrorReporter() - val viewModel = ReceiverHomeViewModel(repository, reporter) + val viewModel = + ReceiverHomeViewModel( + receiverRepository = repository, + mindRecordReceiverRepository = repository.mindRecordReceiverRepository, + receiverTimeLetterRepository = repository.receiverTimeLetterRepository, + errorReporter = reporter, + ) composeRule.setContent { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value @@ -281,7 +291,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( @@ -296,8 +306,9 @@ class ReceiverRuntimeCompletionAndroidTest { .onNodeWithText(context.getString(R.string.receiver_home_sender_record_title, "이발신")) .assertIsDisplayed() composeRule.onNodeWithText("언제나 응원할게").assertIsDisplayed() + // 마음의 기록 실패 섹션은 가짜 0 대신 조회 실패 문구로 구분된다. composeRule - .onNodeWithText("0개 마음의 기록이 있습니다.") + .onNodeWithText(context.getString(R.string.receiver_home_section_count_unavailable)) .assertIsDisplayed() composeRule .onNodeWithText("2개의 애프터노트가 있습니다.") @@ -629,7 +640,7 @@ class ReceiverRuntimeCompletionAndroidTest { composeRule.setContent { AfternoteTheme { - ReceivedAfternoteDetailRoute(onBack = {}, viewModel = viewModel) + ReceivedAfternoteDetailRoute(onBack = {}, onNavigateToPlaylist = {}, viewModel = viewModel) } } @@ -668,7 +679,7 @@ class ReceiverRuntimeCompletionAndroidTest { receiverRepository = repository, errorReporter = FakeErrorReporter(), ) - val playlistRoutes = mutableListOf() + val playlistRoutes = mutableListOf() composeRule.setContent { AfternoteTheme { @@ -689,21 +700,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) @@ -718,8 +729,8 @@ private class CompletionReceiverRepository : ReceiverRepository { override val authCodeFlow: Flow = authCode private val afterNoteHomeResults = ArrayDeque>>() - private val mindRecordHomeResults = ArrayDeque>>() - private val timeLetterHomeResults = ArrayDeque>>() + private val mindRecordHomeResults = ArrayDeque>>() + private val timeLetterHomeResults = ArrayDeque>>() private val senderMessageHomeResults = ArrayDeque>>() val detailResults = ArrayDeque>() @@ -771,20 +782,31 @@ private class CompletionReceiverRepository : ReceiverRepository { error("unexpected saveReceivedExportToFile") } - override suspend fun loadMindRecordsCount(): Result { - homeCallCounts[1] += 1 - return mindRecordHomeResults.removeFirst().await() - } - - override suspend fun loadTimeLettersCount(): Result { - homeCallCounts[2] += 1 - return timeLetterHomeResults.removeFirst().await() - } - override suspend fun loadSenderMessage(): Result { homeCallCounts[3] += 1 return senderMessageHomeResults.removeFirst().await() } + + /** 홈의 마음의 기록 카운트 — 옛 loadMindRecordsCount 가 분리돼 나간 현행 repository 자리. */ + val mindRecordReceiverRepository = + object : MindRecordReceiverRepository { + override suspend fun getAll(): Result { + homeCallCounts[1] += 1 + return mindRecordHomeResults.removeFirst().await() + } + } + + /** 홈의 타임레터 카운트 — 현행 인터페이스는 Result 대신 예외로 실패를 알린다. */ + val receiverTimeLetterRepository = + object : ReceiverTimeLetterRepository { + override suspend fun getReceivedTimeLetters(): ReceivedTimeLetterList { + homeCallCounts[2] += 1 + return timeLetterHomeResults.removeFirst().await().getOrThrow() + } + + override suspend fun getReceivedTimeLetterDetail(timeLetterReceiverId: Long): ReceivedTimeLetter = + error("unexpected getReceivedTimeLetterDetail") + } } private class CompletionIdentityVerificationRepository : IdentityVerificationRepository { 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..522d47ac0a 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 @@ -188,6 +187,7 @@ class ReceiverAdvancedAndroidTest { AfternoteTheme { ReceivedAfternoteDetailRoute( onBack = {}, + onNavigateToPlaylist = {}, viewModel = viewModel, ) } @@ -319,10 +319,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..a9f914ec84 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 @@ -316,7 +317,7 @@ class SettingAccountSecurityAndroidTest { } @Test - fun withdrawFailure_requiresFinalConfirmationAndKeepsSession() { + fun withdrawFailure_showsFailurePopupAndKeepsSession() { val authRepository = SettingContractAuthRepository() val userRepository = SettingContractUserRepository().apply { @@ -341,15 +342,12 @@ 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 } + composeRule.onNodeWithText("회원 탈퇴에 실패했습니다. 잠시 후 다시 시도해 주세요.").assertIsDisplayed() - assertFalse(viewModel.withdrawCompleted.value) + assertEquals(WithdrawUiState.Error, viewModel.withdrawUiState.value) 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 95aa65b8a87de482f94c55cab87a901fc2327dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 23 Aug 2026 21:16:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20PR=20unit-test=20=EC=97=90=20:app:com?= =?UTF-8?q?pileDebugAndroidTestKotlin=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 를 다루는 워크플로는 android-managed-device.yml(수동+주 2회 스케줄)뿐이라 PR 단계엔 androidTest 컴파일 검증이 전무했다. 실행까진 못 해도 컴파일 깨짐은 PR 에서 잡도록 기존 unit-test 잡의 Gradle 호출에 태스크만 추가. --- .github/workflows/unit-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 50baf2a844..ec40f31bad 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,6 +5,10 @@ name: Unit Test # - :koverXmlReportCi / :koverHtmlReportCi : Android debug + 순수 JVM 테스트와 aggregate coverage # - :konsist:test : 순수 JVM 모듈(testDebugUnitTest 미보유)이라 별도 명시. 레이어 의존 아키텍처 가드. # - :build-logic:test : included build 라 루트 태스크에 안 묶여 별도 명시. release 키 가드(TestKit) 회귀 가드. +# - :app:compileDebugAndroidTestKotlin : androidTest 실행은 정기 워크플로(android-managed-device.yml) +# 뿐이라 PR 단계엔 androidTest 컴파일 검증이 없었다. 그 공백으로 #861/#867 androidTest 배치가 +# develop 의 시그니처 변경(#868)과 조합 컴파일이 깨진 채 머지됨 — 실행은 못 해도 컴파일은 여기서 잡는다. +# androidTest 소스셋은 :app 에만 있다. # lint.yml 과 setup 이 거의 동일하지만 분리 — lint 실패가 test 결과를 가리지 않도록 fail-fast 독립. # base 필터를 두지 않는다 — base 변경(feat/* → develop)은 pull_request 기본 activity type 에 @@ -52,7 +56,7 @@ jobs: 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 + run: ./gradlew :koverXmlReportCi :koverHtmlReportCi :konsist:test :build-logic:test :app:compileDebugAndroidTestKotlin --continue --parallel - name: Upload Kover coverage reports id: kover-artifact