From ba5ba3278d1e3baa77c72c62472917e5973db62a Mon Sep 17 00:00:00 2001 From: WeiHe Date: Sun, 10 Dec 2023 16:42:20 +0800 Subject: [PATCH] test(app navigation): Add app navigation end to end test --- .../com/wei/picquest/ui/NavigationRobot.kt | 83 ++++++++++++++++++ .../com/wei/picquest/ui/NavigationTest.kt | 87 +++++++++++++++++++ .../picquest/ui/robot/HomeEndToEndRobot.kt | 42 +++++++++ 3 files changed, 212 insertions(+) create mode 100644 app/src/androidTest/java/com/wei/picquest/ui/NavigationRobot.kt create mode 100644 app/src/androidTest/java/com/wei/picquest/ui/NavigationTest.kt create mode 100644 app/src/androidTest/java/com/wei/picquest/ui/robot/HomeEndToEndRobot.kt diff --git a/app/src/androidTest/java/com/wei/picquest/ui/NavigationRobot.kt b/app/src/androidTest/java/com/wei/picquest/ui/NavigationRobot.kt new file mode 100644 index 0000000..90d4688 --- /dev/null +++ b/app/src/androidTest/java/com/wei/picquest/ui/NavigationRobot.kt @@ -0,0 +1,83 @@ +import androidx.annotation.StringRes +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.wei.picquest.MainActivity +import kotlin.properties.ReadOnlyProperty + +/** + * Robot for [NavigationTest]. + * + * 遵循此模型,找到測試使用者介面元素、檢查其屬性、和透過測試規則執行動作: + * composeTestRule{.finder}{.assertion}{.action} + * + * Testing cheatsheet: + * https://developer.android.com/jetpack/compose/testing-cheatsheet + */ +internal fun navigationRobot( + composeTestRule: AndroidComposeTestRule, MainActivity>, + func: NavigationRobot.() -> Unit, +) = NavigationRobot(composeTestRule).apply(func) + +internal open class NavigationRobot( + private val composeTestRule: AndroidComposeTestRule, MainActivity>, +) { + private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = + ReadOnlyProperty { _, _ -> activity.getString(resId) } + + // The strings used for matching in these tests + private val home by composeTestRule.stringResource(com.wei.picquest.R.string.home) + private val photo by composeTestRule.stringResource(com.wei.picquest.R.string.photo) + private val video by composeTestRule.stringResource(com.wei.picquest.R.string.video) + private val contactMe by composeTestRule.stringResource(com.wei.picquest.R.string.contact_me) + + private val navHome by lazy { + composeTestRule.onNodeWithContentDescription( + home, + useUnmergedTree = true, + ) + } + private val navPhoto by lazy { + composeTestRule.onNodeWithContentDescription( + photo, + useUnmergedTree = true, + ) + } + private val navVideo by lazy { + composeTestRule.onNodeWithContentDescription( + video, + useUnmergedTree = true, + ) + } + private val navContactMe by lazy { + composeTestRule.onNodeWithContentDescription( + contactMe, + useUnmergedTree = true, + ) + } + + internal fun clickNavHome() { + navHome.performClick() + // 等待任何動畫完成 + composeTestRule.waitForIdle() + } + + internal fun clickNavPhoto() { + navPhoto.performClick() + // 等待任何動畫完成 + composeTestRule.waitForIdle() + } + + internal fun clickVideo() { + navVideo.performClick() + // 等待任何動畫完成 + composeTestRule.waitForIdle() + } + + internal fun clickNavContactMe() { + navContactMe.performClick() + // 等待任何動畫完成 + composeTestRule.waitForIdle() + } +} diff --git a/app/src/androidTest/java/com/wei/picquest/ui/NavigationTest.kt b/app/src/androidTest/java/com/wei/picquest/ui/NavigationTest.kt new file mode 100644 index 0000000..fbbbee0 --- /dev/null +++ b/app/src/androidTest/java/com/wei/picquest/ui/NavigationTest.kt @@ -0,0 +1,87 @@ +package com.wei.picquest.ui + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.espresso.Espresso +import androidx.test.espresso.NoActivityResumedException +import com.wei.picquest.MainActivity +import com.wei.picquest.ui.robot.homeEndToEndRobot +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import navigationRobot +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +/** + * Tests all the navigation flows that are handled by the navigation library. + */ +@HiltAndroidTest +class NavigationTest { + + /** + * Manages the components' state and is used to perform injection on your test + */ + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + /** + * Create a temporary folder used to create a Data Store file. This guarantees that + * the file is removed in between each test, preventing a crash. + */ + @BindValue + @get:Rule(order = 1) + val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + /** + * Use the primary activity to initialize the app normally. + */ + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + @Before + fun setup() = hiltRule.inject() + + @Test + fun firstScreen_isHome() { + homeEndToEndRobot(composeTestRule) { + verifyImportantNoteDisplayed() + } + } + + /* + * 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_returnsToHome() { + homeEndToEndRobot(composeTestRule) { + navigationRobot(composeTestRule) { + // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. + clickNavContactMe() + // WHEN the user uses the system button/gesture to go back + Espresso.pressBack() + } + verifyImportantNoteDisplayed() + } + } + + /* + * There should always be at most one instance of a top-level destination at the same time. + */ + @Test(expected = NoActivityResumedException::class) + fun topDestination_back_quitsApp() { + homeEndToEndRobot(composeTestRule) { + navigationRobot(composeTestRule) { + // GIVEN the user navigates to the Contact Me destination + clickNavContactMe() + // 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/picquest/ui/robot/HomeEndToEndRobot.kt b/app/src/androidTest/java/com/wei/picquest/ui/robot/HomeEndToEndRobot.kt new file mode 100644 index 0000000..795553a --- /dev/null +++ b/app/src/androidTest/java/com/wei/picquest/ui/robot/HomeEndToEndRobot.kt @@ -0,0 +1,42 @@ +package com.wei.picquest.ui.robot + +import androidx.annotation.StringRes +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.picquest.MainActivity +import kotlin.properties.ReadOnlyProperty +import com.wei.picquest.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) } + + // The strings used for matching in these tests + private val importantNoteDescription by composeTestRule.stringResource(FeatureHomeR.string.important_note) + + private val importantNote by lazy { + composeTestRule.onNode(hasContentDescription(importantNoteDescription)) + } + + fun verifyImportantNoteDisplayed() { + importantNote.assertExists().assertIsDisplayed() + } +}