diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt index efbd6da3..5e61d9e5 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt @@ -89,8 +89,8 @@ class AtAppStateTest { } assertThat(state.topLevelDestinations).hasSize(3) - assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("schedule") - assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("home") + assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("home") + assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("schedule") assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("contact_me") } diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt index e3ea92db..6c4c48b6 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt @@ -68,7 +68,7 @@ internal open class NavigationRobot( composeTestRule.waitForIdle() } - private fun clickNavHome() { + internal fun clickNavHome() { navHome.performClick() // 等待任何動畫完成 composeTestRule.waitForIdle() diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt index fb4b426d..9f5f37cf 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt @@ -51,11 +51,11 @@ class NavigationTest { } @Test - fun isScheduleScreen_afterLogin() { + fun isHomeScreen_afterLogin() { welcomeEndToEndRobot(composeTestRule) { } getStartedClick { } login { - verifyScheduleTopAppBarDisplayed() + verifyMenuButtonDisplayed() } } @@ -77,11 +77,11 @@ class NavigationTest { } /* - * When pressing back from any top level destination except "Schedule", the app navigates back - * to the "Schedule" destination, no matter which destinations you visited in between. + * When pressing back from any top level destination except "Home", the app navigates back + * to the "Home" destination, no matter which destinations you visited in between. */ @Test - fun navigationBar_backFromAnyDestination_returnsToSchedule() { + fun navigationBar_backFromAnyDestination_returnsToHome() { welcomeEndToEndRobot(composeTestRule) { } getStartedClick { } login { @@ -91,7 +91,7 @@ class NavigationTest { // WHEN the user uses the system button/gesture to go back Espresso.pressBack() } - verifyScheduleTopAppBarDisplayed() + verifyMenuButtonDisplayed() } } @@ -106,8 +106,8 @@ class NavigationTest { navigationRobot(composeTestRule) { // GIVEN the user navigates to the Contact Me destination clickNavContactMe() - // and then navigates to the Schedule destination - clickNavSchedule() + // and then navigates to the Home destination + clickNavHome() // WHEN the user uses the system button/gesture to go back Espresso.pressBack() // THEN the app quits diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt new file mode 100644 index 00000000..6c7d8437 --- /dev/null +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt @@ -0,0 +1,63 @@ +package com.wei.amazingtalker.ui.robot + +import androidx.annotation.StringRes +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.wei.amazingtalker.MainActivity +import kotlin.properties.ReadOnlyProperty +import com.wei.amazingtalker.feature.home.R as FeatureHomeR + +/** + * Screen Robot for End To End Test. + * + * 遵循此模型,找到測試使用者介面元素、檢查其屬性、和透過測試規則執行動作: + * composeTestRule{.finder}{.assertion}{.action} + * + * Testing cheatsheet: + * https://developer.android.com/jetpack/compose/testing-cheatsheet + */ +internal fun homeEndToEndRobot( + composeTestRule: AndroidComposeTestRule, MainActivity>, + func: HomeEndToEndRobot.() -> Unit, +) = HomeEndToEndRobot(composeTestRule).apply(func) + +internal open class HomeEndToEndRobot( + private val composeTestRule: AndroidComposeTestRule, MainActivity>, +) { + private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = + ReadOnlyProperty { _, _ -> activity.getString(resId) } + + private fun withRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { + val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false + roleProperty == role + } + + // The strings used for matching in these tests + private val menuDescription by composeTestRule.stringResource(FeatureHomeR.string.menu) + + private val menuButton by lazy { + composeTestRule.onNode( + withRole(Role.Button) + .and(hasContentDescription(menuDescription)), + ) + } + + fun verifyMenuButtonDisplayed() { + menuButton.assertExists().assertIsDisplayed() + } + + fun isMenuButtonDisplayed(): Boolean { + return try { + verifyMenuButtonDisplayed() + true + } catch (e: AssertionError) { + false + } + } +} diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt index 6fbe5b41..2859fdf3 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt @@ -63,11 +63,11 @@ internal open class LoginEndToEndRobotRobot( } } - infix fun login(func: ScheduleEndToEndRobot.() -> Unit): ScheduleEndToEndRobot { + infix fun login(func: HomeEndToEndRobot.() -> Unit): HomeEndToEndRobot { loginButton.performClick() - return scheduleEndToEndRobot(composeTestRule) { + return homeEndToEndRobot(composeTestRule) { // 等待任何動畫完成 - composeTestRule.waitUntil(3_000) { isScheduleTopAppBarDisplayed() } + composeTestRule.waitUntil(3_000) { isMenuButtonDisplayed() } func() } } diff --git a/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt b/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt index ee4358a4..fabe8932 100644 --- a/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt +++ b/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt @@ -7,11 +7,11 @@ import androidx.window.layout.DisplayFeature import com.wei.amazingtalker.core.designsystem.ui.DeviceOrientation import com.wei.amazingtalker.feature.contactme.contactme.navigation.contactMeScreen import com.wei.amazingtalker.feature.home.home.navigation.homeGraph +import com.wei.amazingtalker.feature.home.home.navigation.homeRoute import com.wei.amazingtalker.feature.login.login.navigation.loginScreen import com.wei.amazingtalker.feature.login.welcome.navigation.welcomeGraph import com.wei.amazingtalker.feature.login.welcome.navigation.welcomeRoute import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.scheduleGraph -import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.scheduleRoute import com.wei.amazingtalker.feature.teacherschedule.scheduledetail.navigation.scheduleDetailScreen import com.wei.amazingtalker.ui.AtAppState @@ -28,7 +28,7 @@ fun AtNavHost( appState: AtAppState, isTokenValid: Boolean, displayFeatures: List, - startDestination: String = if (isTokenValid) scheduleRoute else welcomeRoute, + startDestination: String = if (isTokenValid) homeRoute else welcomeRoute, ) { val navController = appState.navController val navigationType = appState.navigationType @@ -49,6 +49,10 @@ fun AtNavHost( ) }, ) + homeGraph( + navController = navController, + tokenInvalidNavigate = { appState.tokenInvalidNavigate() }, + ) scheduleGraph( navController = navController, tokenInvalidNavigate = { appState.tokenInvalidNavigate() }, @@ -56,7 +60,6 @@ fun AtNavHost( scheduleDetailScreen(navController = navController) }, ) - homeGraph() contactMeScreen( navController = navController, contentType = contentType, diff --git a/app/src/main/java/com/wei/amazingtalker/navigation/TopLevelDestination.kt b/app/src/main/java/com/wei/amazingtalker/navigation/TopLevelDestination.kt index 2be93398..b3f90a50 100644 --- a/app/src/main/java/com/wei/amazingtalker/navigation/TopLevelDestination.kt +++ b/app/src/main/java/com/wei/amazingtalker/navigation/TopLevelDestination.kt @@ -15,18 +15,18 @@ enum class TopLevelDestination( val iconTextId: Int, val titleTextId: Int, ) { - SCHEDULE( - selectedIcon = AtIcons.Schedule, - unselectedIcon = AtIcons.ScheduleBorder, - iconTextId = R.string.schedule, - titleTextId = R.string.schedule, - ), HOME( selectedIcon = AtIcons.Home, unselectedIcon = AtIcons.HomeBorder, iconTextId = R.string.home, titleTextId = R.string.home, ), + SCHEDULE( + selectedIcon = AtIcons.Schedule, + unselectedIcon = AtIcons.ScheduleBorder, + iconTextId = R.string.schedule, + titleTextId = R.string.schedule, + ), CONTACT_ME( selectedIcon = AtIcons.ContactMe, unselectedIcon = AtIcons.ContactMeBorder, diff --git a/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt b/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt index 32e2c174..18e40407 100644 --- a/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt +++ b/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt @@ -159,9 +159,9 @@ class AtAppState( val currentTopLevelDestination: TopLevelDestination? @Composable get() = when (currentDestination?.route) { + homeRoute -> TopLevelDestination.HOME scheduleRoute -> TopLevelDestination.SCHEDULE contactMeRoute -> TopLevelDestination.CONTACT_ME - homeRoute -> TopLevelDestination.HOME else -> null } @@ -205,11 +205,11 @@ class AtAppState( } when (topLevelDestination) { - TopLevelDestination.SCHEDULE -> navController.navigateToSchedule( + TopLevelDestination.HOME -> navController.navigateToHome( topLevelNavOptions, ) - TopLevelDestination.HOME -> navController.navigateToHome( + TopLevelDestination.SCHEDULE -> navController.navigateToSchedule( topLevelNavOptions, ) @@ -224,7 +224,7 @@ class AtAppState( fun loginNavigate() { navController.popBackStack() - navController.navigateToSchedule() + navController.navigateToHome() } fun tokenInvalidNavigate() { diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt index 8c7016d8..988cc018 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import coil.ImageLoader import coil.compose.rememberAsyncImagePainter import coil.decode.SvgDecoder @@ -88,7 +89,10 @@ import com.wei.amazingtalker.feature.home.home.ui.StatusCard * */ @Composable -internal fun HomeRoute() { +internal fun HomeRoute( + navController: NavController, + tokenInvalidNavigate: () -> Unit, +) { HomeScreen() } @@ -310,12 +314,14 @@ fun loadImageUsingCoil(resId: Int, isPreview: Boolean): Painter { private fun MenuButton( onMenuClick: () -> Unit, ) { + val menu = stringResource(R.string.menu) + IconButton( onClick = onMenuClick, modifier = Modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant) - .semantics { contentDescription = "" }, + .semantics { contentDescription = menu }, ) { Icon( imageVector = AtIcons.Menu, diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt index ef061cd9..decbc231 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt @@ -12,8 +12,14 @@ fun NavController.navigateToHome(navOptions: NavOptions? = null) { this.navigate(homeRoute, navOptions) } -fun NavGraphBuilder.homeGraph() { +fun NavGraphBuilder.homeGraph( + navController: NavController, + tokenInvalidNavigate: () -> Unit, +) { composable(route = homeRoute) { - HomeRoute() + HomeRoute( + navController = navController, + tokenInvalidNavigate = tokenInvalidNavigate, + ) } } diff --git a/feature/home/src/main/res/values-zh-rTW/strings.xml b/feature/home/src/main/res/values-zh-rTW/strings.xml index 017515ec..d3e2a3c8 100644 --- a/feature/home/src/main/res/values-zh-rTW/strings.xml +++ b/feature/home/src/main/res/values-zh-rTW/strings.xml @@ -1,4 +1,6 @@ + %s 的大頭貼 + 選單 \ No newline at end of file diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index e6feaaa5..0c9f558b 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ + %s\'s profile picture + Menu \ No newline at end of file