diff --git a/feature/receiver/presentation/build.gradle.kts b/feature/receiver/presentation/build.gradle.kts index b9a74a15d8..e45f27fa55 100644 --- a/feature/receiver/presentation/build.gradle.kts +++ b/feature/receiver/presentation/build.gradle.kts @@ -6,6 +6,8 @@ plugins { } android { + testOptions.unitTests.isIncludeAndroidResources = true + namespace = "com.afternote.feature.receiver.presentation" experimentalProperties["android.experimental.enableScreenshotTest"] = true } @@ -36,6 +38,10 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.robolectric) + // 실패 표기가 «레이아웃을 유지하는지» 는 실제로 그려 봐야 확인된다 (#952). + testImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.test.manifest) + screenshotTestImplementation(libs.screenshot.validation.api) screenshotTestImplementation(libs.androidx.compose.ui.tooling) } diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt index 903620a00f..fb949c1d12 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt @@ -48,14 +48,10 @@ fun AfternoteSection( title = stringResource(R.string.receiver_home_afternote_section_title), description = stringResource(R.string.receiver_home_afternote_section_desc), countLine = - if (totalCount == null) { - unavailableCountLine() - } else { - rememberCountLine( - prefix = "${totalCount}개", - suffix = "의 애프터노트가 있습니다.", - ) - }, + rememberCountLine( + prefix = "${countText(totalCount)}개", + suffix = "의 애프터노트가 있습니다.", + ), buttonText = stringResource(R.string.receiver_home_afternote_section_button), onButtonClick = onGoClick, middleContent = { diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt index 0988f205ef..423b3a46e3 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt @@ -88,6 +88,12 @@ fun rememberCountLine( append(suffix) } -/** 조회 실패를 정상 0건과 구분하는 중립 문구. */ +/** + * 개수 자리에 넣을 문자열 — 값이 있으면 숫자, 없으면 대시 (#952). + * + * 갈래를 문장이 아니라 **개수 자리 하나로** 좁힌다. 문장을 통째로 갈아 끼우면 섹션 높이와 + * 줄 구성이 바뀌고(시안 확정값은 레이아웃 유지), 호출부마다 같은 문장이 if/else 양쪽에 + * 두 번 적혀 한쪽만 고쳐질 자리가 된다. + */ @Composable -fun unavailableCountLine(): AnnotatedString = AnnotatedString(stringResource(R.string.receiver_home_section_count_unavailable)) +fun countText(count: Int?): String = count?.toString() ?: stringResource(R.string.receiver_home_section_count_unavailable) diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt index 07dca23183..8a70956b2b 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt @@ -31,18 +31,15 @@ fun MindRecordSection( title = stringResource(R.string.receiver_home_mindrecord_section_title), description = stringResource(R.string.receiver_home_mindrecord_section_desc), countLine = - if (summary == null) { - unavailableCountLine() - } else { - rememberCountLine( - prefix = "${summary.totalCount}개 ", - suffix = "마음의 기록이 있습니다.", - ) - }, + rememberCountLine( + prefix = "${countText(summary?.totalCount)}개 ", + suffix = "마음의 기록이 있습니다.", + ), buttonText = stringResource(R.string.receiver_home_mindrecord_section_button), onButtonClick = onGoClick, middleContent = { - if (summary == null) return@HomeSectionCard + // 조회 실패에도 카드를 숨기지 않는다 — 섹션 레이아웃 유지가 시안 확정값이고, + // 통째로 사라지면 «기능이 없는 것» 과 «못 불러온 것» 이 구분되지 않는다 (#952). Spacer(modifier = Modifier.height(16.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -52,14 +49,14 @@ fun MindRecordSection( iconResId = CoreUiR.drawable.core_ui_ic_mindrecord, label = stringResource(R.string.receiver_home_mindrecord_daily_question), totalLabel = totalLabel, - count = summary.dailyQuestionCount, + count = summary?.dailyQuestionCount, modifier = Modifier.weight(1f), ) MindRecordStatCard( iconResId = CoreUiR.drawable.core_ui_ic_diary, label = stringResource(R.string.receiver_home_mindrecord_diary), totalLabel = totalLabel, - count = summary.diaryCount, + count = summary?.diaryCount, modifier = Modifier.weight(1f), ) } diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt index dd9c4f131b..d1e811e7ca 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt @@ -20,11 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.receiver.presentation.R import com.afternote.core.ui.R as CoreUiR /** @@ -35,7 +37,8 @@ fun MindRecordStatCard( iconResId: Int, label: String, totalLabel: String, - count: Int, + /** 조회 실패·미상이면 null — 카드를 숨기지 않고 개수 자리만 대시로 둔다 (#952). */ + count: Int?, modifier: Modifier = Modifier, ) { Column( @@ -81,7 +84,7 @@ fun MindRecordStatCard( ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = count.toString(), + text = count?.toString() ?: stringResource(R.string.receiver_home_section_count_unavailable), style = AfternoteDesign.typography.h3, color = AfternoteDesign.colors.gray9, ) diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt index 98f94ad6bd..36d58b15d4 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt @@ -21,14 +21,10 @@ fun TimeLetterSection( title = stringResource(R.string.receiver_home_timeletter_section_title), description = stringResource(R.string.receiver_home_timeletter_section_desc), countLine = - if (totalCount == null) { - unavailableCountLine() - } else { - rememberCountLine( - prefix = "${totalCount}개 ", - suffix = "라이프 이벤트 레터가 있습니다.", - ) - }, + rememberCountLine( + prefix = "${countText(totalCount)}개 ", + suffix = "라이프 이벤트 레터가 있습니다.", + ), buttonText = stringResource(R.string.receiver_home_timeletter_section_button), onButtonClick = onGoClick, ) diff --git a/feature/receiver/presentation/src/main/res/values/strings.xml b/feature/receiver/presentation/src/main/res/values/strings.xml index 5df3cbc3da..6ba371513f 100644 --- a/feature/receiver/presentation/src/main/res/values/strings.xml +++ b/feature/receiver/presentation/src/main/res/values/strings.xml @@ -14,7 +14,12 @@ 애프터노트 고인이 사후 정리하고자 하는 데이터입니다. 애프터노트 확인하러 가기 - 기록 수를 불러오지 못했습니다. + + – 모든 기록 내려받기 모든 기록을 내려받으시겠습니까? 모든 기록 내려받기에 실패했습니다. diff --git a/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png b/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png index d8dc314511..919e55998f 100644 Binary files a/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png and b/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png differ diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt new file mode 100644 index 0000000000..963eac5b38 --- /dev/null +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt @@ -0,0 +1,69 @@ +package com.afternote.feature.receiver.presentation.home.component + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.receiver.presentation.home.model.MindRecordSummary +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * 기록 수 조회 실패 표기 (#952, 시안 4309:19394). + * + * 확정값은 «개수 자리만 대시, 레이아웃은 유지» 다. 종전에는 문장을 통째로 "기록 수를 + * 불러오지 못했습니다." 로 갈아 끼우고 통계 카드 행을 숨겨, 실패한 화면과 정상 화면의 + * 구성이 달랐다 — 카드가 통째로 사라지면 «기능이 없는 것» 과 «못 불러온 것» 도 + * 구분되지 않는다. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class ReceiverHomeCountFallbackTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun `조회에 실패해도 문장 구조가 그대로다`() { + composeRule.setContent { + AfternoteTheme { MindRecordSection(summary = null, onGoClick = {}) } + } + + // 종전 문구가 아니라, 개수 자리만 바뀐 같은 문장이어야 한다. + composeRule.onNodeWithText("–개 마음의 기록이 있습니다.").assertIsDisplayed() + } + + @Test + fun `조회에 실패해도 통계 카드가 남는다`() { + composeRule.setContent { + AfternoteTheme { MindRecordSection(summary = null, onGoClick = {}) } + } + + composeRule.onNodeWithText("데일리 질문").assertIsDisplayed() + composeRule.onNodeWithText("일기").assertIsDisplayed() + // 두 카드의 개수 자리가 «모두» 대시다 — 인덱스 하나만 보면 한 장만 대시로 남는 + // 회귀를 놓치고, 노드 순서가 바뀌면 조용히 다른 것을 본다. + composeRule.onAllNodesWithText("–").assertCountEquals(2) + } + + @Test + fun `정상 조회는 숫자를 그대로 보여준다`() { + composeRule.setContent { + AfternoteTheme { + MindRecordSection( + summary = MindRecordSummary(dailyQuestionCount = 18, diaryCount = 4), + onGoClick = {}, + ) + } + } + + composeRule.onNodeWithText("22개 마음의 기록이 있습니다.").assertIsDisplayed() + composeRule.onNodeWithText("18").assertIsDisplayed() + composeRule.onNodeWithText("4").assertIsDisplayed() + } +}