diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt index 0e6c4c5742..8b24a9d6aa 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt @@ -13,11 +13,17 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.R +/** 프로필 아이콘 앵커. 장식이라 semantics 이름이 없어 테스트가 이 태그로 존재 여부를 본다. */ +const val PROFILE_ICON_TEST_TAG = "home_top_bar_profile" + @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeTopBar( @@ -44,10 +50,13 @@ fun HomeTopBar( .padding(end = 25.dp), ) { if (showProfileIcon) { + // 목적지가 없어 어디서도 눌리지 않는다 — 장식이므로 이름을 붙이지 않는다. + // 이름을 주면 TalkBack 이 포커스 가능한 노드로 읽어, 사용자가 액션이 있다고 + // 믿고 탭했다가 아무 반응도 얻지 못한다 (#613 리뷰). Image( painter = painterResource(R.drawable.core_ui_user), contentDescription = null, - modifier = Modifier.size(18.dp), + modifier = Modifier.size(18.dp).testTag(PROFILE_ICON_TEST_TAG), ) Spacer(modifier = Modifier.width(15.dp)) @@ -55,11 +64,13 @@ fun HomeTopBar( Image( painter = painterResource(R.drawable.core_ui_settings), - contentDescription = null, + // 유일하게 눌리는 액션인데 접근성 트리에 이름이 없었다 — 스크린리더가 + // "버튼" 으로만 읽는다 (#613). + contentDescription = stringResource(R.string.core_ui_home_top_bar_setting), modifier = Modifier .size(18.dp) - .clickable { onSettingClick() }, + .clickable(role = Role.Button) { onSettingClick() }, ) } }, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index e30bac9ebe..caa01e4e4d 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -26,4 +26,6 @@ 네트워크 연결 오류 인터넷 연결을 확인한 후 다시 시도해 주세요. 다시 시도하기 + + 설정 \ No newline at end of file diff --git a/docs/qa/assumptions.md b/docs/qa/assumptions.md index d975e56e8b..0e2a212d3c 100644 --- a/docs/qa/assumptions.md +++ b/docs/qa/assumptions.md @@ -259,6 +259,15 @@ Compose 테스트로 고정했다. 스키마에 `date` 없음, 보내도 무시됨을 실측). 날짜 선택이 표시 전용인 문제는 BE 계약 확장이나 기획 결정이 필요해 #1008 로 분리했다. +## #613 — 수신자 홈 설정 진입 (판단 보류) + +- 시안(수신자 섹션 `4327:73626`)의 수신자 홈 헤더에는 **톱니만** 있고 프로필 아이콘이 없다. + 코드도 이미 `showProfileIcon = false` 라 1번 지적은 해소된 상태였고, 재발 방지 가드만 붙였다. +- 톱니의 목적지는 확정하지 못했다. 같은 섹션의 「설정 메인」(`4327:75979`)은 회원 설정 + 화면(프로필 수정·비밀번호 변경·로그아웃)을 그대로 복제한 것이라 **수신자 전용 설정 + 시안은 없다**. 수신자는 X-Auth-Code 기반이라 로그아웃할 세션이 없다. +- 현 배선은 회원 설정 그래프(`Route.Setting`)로 그대로 보낸다. 무엇을 남길지(고객지원만, + 인증 해제, 진입점 제거)는 기획·디자인 확정이 필요해 이번 PR 범위에서 제외했다. ## #793 — MEMORIES 카드 「그날의 기록 다시 읽기」 목적지 (임시 승계) - **최종 목적지는 여전히 미확정이다.** Figma 카드 노드(`109:15028`)의 이 버튼에는 프로토타입 diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt new file mode 100644 index 0000000000..e5a603be18 --- /dev/null +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt @@ -0,0 +1,69 @@ +package com.afternote.feature.receiver.presentation.home + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import com.afternote.core.ui.R +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.core.ui.topbar.PROFILE_ICON_TEST_TAG +import com.afternote.feature.receiver.presentation.home.model.ReceiverHomeUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * 수신자 홈 헤더가 수신자가 실제로 할 수 있는 액션만 두는지 (#613, 시안 4327:73626). + * + * 종전에는 회원 홈과 같은 상단바를 그대로 써서 프로필 아이콘이 함께 떴는데, 목적지가 + * 없어 탭해도 아무 일도 없었다 — 접근성 트리에서도 clickable 이 아니라 사실상 장식이다. + * 시안의 수신자 홈에도 톱니 하나뿐이라 그 상태를 고정한다. + * + * 설정 아이콘의 이름도 함께 고정한다. 종전에는 둘 다 `contentDescription = null` 이라 + * 스크린리더가 **유일하게 눌리는 액션**조차 읽어 주지 못했다. 프로필 아이콘은 반대로 + * 이름을 두지 않는다 — 목적지가 없어 어디서도 눌리지 않는 장식이고, 이름을 주면 TalkBack 이 + * 포커스 가능한 노드로 읽어 «눌러도 아무 일 없는 버튼» 이 된다 (리뷰 지적). + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class ReceiverHomeHeaderTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun `수신자 홈에는 프로필 아이콘이 없다`() { + composeRule.setContent { + AfternoteTheme { + ReceiverHomeScreen( + uiState = ReceiverHomeUiState.Loading, + onEvent = {}, + actions = ReceiverHomeActions(), + ) + } + } + + // 프로필 아이콘은 목적지가 없는 장식이라 semantics 이름이 없다 — 테스트 태그로 본다. + composeRule.onNodeWithTag(PROFILE_ICON_TEST_TAG).assertDoesNotExist() + } + + @Test + fun `설정 아이콘은 이름이 있고 눌린다`() { + var settingClicks = 0 + composeRule.setContent { + AfternoteTheme { + ReceiverHomeScreen( + uiState = ReceiverHomeUiState.Loading, + onEvent = {}, + actions = ReceiverHomeActions(onSettingClick = { settingClicks += 1 }), + ) + } + } + + val setting = composeRule.activity.getString(R.string.core_ui_home_top_bar_setting) + composeRule.onNodeWithContentDescription(setting).assertIsDisplayed().assertHasClickAction() + } +}