From 1e3520dddf82e12b4ea7e9e3e2bf5e9c74bd116a Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Thu, 23 Dec 2021 17:24:56 +0100 Subject: [PATCH] [NiA] Implement top level navigation Change-Id: I84ed2330de00c3644db644e33351daaf9bb3afed --- app/build.gradle | 8 +- .../apps/nowinandroid/ui/NavigationTest.kt | 176 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 10 +- .../samples/apps/nowinandroid/MainActivity.kt | 31 +-- .../samples/apps/nowinandroid/ui/NiaApp.kt | 159 ++++++++++++++++ .../apps/nowinandroid/ui/NiaNavGraph.kt | 57 ++++++ .../apps/nowinandroid/ui/NiaNavigation.kt | 43 +++++ .../apps/nowinandroid/ui/theme/Shape.kt | 27 --- .../apps/nowinandroid/ui/theme/Theme.kt | 44 ++--- .../apps/nowinandroid/ui/theme/Type.kt | 27 +-- app/src/main/res/values-night/themes.xml | 19 +- app/src/main/res/values-v23/themes.xml | 22 +++ app/src/main/res/values-v27/themes.xml | 24 +++ app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/themes.xml | 35 +++- gradle/libs.versions.toml | 10 +- 17 files changed, 568 insertions(+), 143 deletions(-) create mode 100644 app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavGraph.kt create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaNavigation.kt delete mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/ui/theme/Shape.kt create mode 100644 app/src/main/res/values-v23/themes.xml create mode 100644 app/src/main/res/values-v27/themes.xml diff --git a/app/build.gradle b/app/build.gradle index c3ad9ac6c..9638f32f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,8 +79,7 @@ dependencies { implementation libs.androidx.core.ktx implementation libs.androidx.appcompat implementation libs.androidx.lifecycle.viewModelCompose - implementation libs.androidx.navigation.fragment - implementation libs.androidx.navigation.ui.ktx + implementation libs.androidx.navigation.compose implementation libs.material3 implementation libs.accompanist.insets @@ -88,6 +87,7 @@ dependencies { implementation libs.androidx.compose.foundation.layout // TODO (M3): Remove this dependency when all components are available implementation libs.androidx.compose.material + implementation libs.androidx.compose.material.iconsExtended implementation libs.androidx.compose.material3 implementation libs.androidx.compose.ui.tooling implementation libs.androidx.compose.ui.util @@ -101,10 +101,8 @@ dependencies { testImplementation libs.androidx.test.core testImplementation libs.kotlinx.coroutines.test - androidTestImplementation libs.junit4 - androidTestImplementation libs.androidx.test.core androidTestImplementation libs.androidx.test.espresso.core - androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.runner androidTestImplementation libs.androidx.test.rules androidTestImplementation libs.androidx.compose.ui.test diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt new file mode 100644 index 000000000..c7d47fe8a --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.ui + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.espresso.Espresso +import androidx.test.espresso.NoActivityResumedException +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +/** + * Tests all the navigation flows that are handled by the navigation library. + */ +class NavigationTest { + + /** + * Using an empty activity to have control of the content that is set. + */ + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Before + fun setUp() { + // Using targetContext as the Context of the instrumentation code + composeTestRule.setContent { + NiaApp() + } + } + + @Test + fun firstScreenIsForYou() { + composeTestRule.forYouDestinationTopMatcher() + .assertExists("Could not find FOR YOU text on first screen after app open") + } + + // TODO: implement tests related to navigation & resetting of destinations (b/213307564) + /** + * As per guidelines: + * + * When you select a navigation bar item (one that’s not currently selected), the app navigates + * to that destination’s screen. + * + * Any prior user interactions and temporary screen states are reset, such as scroll position, + * tab selection, and inline search. + * + * This default behavior can be overridden when needed to improve the user experience. For + * example, an Android app that requires frequent switching between sections can preserve each + * section’s state. + */ +// @Test +// fun navigateToUnselectedTabResetsContent1() { +// // GIVEN the user was previously on the Topics destination +// composeTestRule.topicsDestinationTopMatcher().performClick() +// // and scrolled down +// [IMPLEMENT] Match the root scrollable container and scroll down to an item below the fold +// composeTestRule.topicsDestinationTopMatcher() +// .assertDoesNotExist() // verify we scrolled beyond the top +// // and then navigated back to the For You destination +// composeTestRule.forYouDestinationTopMatcher().performClick() +// // WHEN the user presses the Topic navigation bar item +// composeTestRule.topicsDestinationTopMatcher().performClick() +// // THEN the Topics destination shows at the top. +// composeTestRule.topicsDestinationTopMatcher() +// .assertExists("Screen did not correctly reset to the top after re-navigating to it") +// } + +// @Test +// fun navigateToUnselectedTabResetsContent2() { +// // GIVEN the user was previously on the Topics destination +// composeTestRule.topicsDestinationTopMatcher().performClick() +// // and navigated to the Topic detail destination +// [IMPLEMENT] Navigate to topic detail destination +// composeTestRule.topicsDestinationTopMatcher() +// .assertDoesNotExist() // verify we are not on topics overview destination any more +// // and then navigated back to the For You destination +// composeTestRule.forYouDestinationTopMatcher().performClick() +// // WHEN the user presses the Topic navigation bar item +// composeTestRule.topicsDestinationTopMatcher().performClick() +// // THEN the Topics destination shows at the top. +// composeTestRule.topicsDestinationTopMatcher() +// .assertExists("Screen did not correctly reset to the top after re-navigating to it") +// } + +// @Test +// fun reselectingTabResetsContent1() { +// // GIVEN the user is on the For You destination +// // and has scrolled down +// // WHEN the user taps the For You navigation bar item +// // THEN the For You destination shows at the top of the destination +// } + +// @Test +// fun reselectingTabResetsContent2() { +// // GIVEN the user is on the Topics destination +// // and navigates to the Topic Detail destination +// // WHEN the user taps the Topics navigation bar item +// // THEN the Topics destination shows at the top of the destination +// } + + /* + * Top level destinations should never show an up affordance. + */ + @Test + fun topLevelDestinationsDoNotShowUpArrow() { + // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. + composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() + composeTestRule.onNodeWithText("Episodes").performClick() + composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() + composeTestRule.onNodeWithText("Saved").performClick() + composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() + composeTestRule.onNodeWithText("Topics").performClick() + composeTestRule.onNodeWithContentDescription("Navigate up").assertDoesNotExist() + } + + /* + * There should always be at most one instance of a top-level destination at the same time. + */ + @Test(expected = NoActivityResumedException::class) + fun backFromHomeDestinationQuitsApp() { + // GIVEN the user navigates to the Episodes destination + composeTestRule.onNodeWithText("Episodes").performClick() + // and then navigates to the For you destination + composeTestRule.onNodeWithText("For you").performClick() + // WHEN the user uses the system button/gesture to go back + Espresso.pressBack() + // THEN the app quits + } + + /* + * When pressing back from any top level destination except "For you", the app navigates back + * to the "For you" destination, no matter which destinations you visited in between. + */ + @Test + fun backFromDestinationReturnsToForYou() { + // GIVEN the user navigated to the Episodes destination + composeTestRule.onNodeWithText("Episodes").performClick() + // and then navigated to the Topics destination + composeTestRule.onNodeWithText("Topics").performClick() + // WHEN the user uses the system button/gesture to go back, + Espresso.pressBack() + // THEN the app shows the For You destination + composeTestRule.forYouDestinationTopMatcher().assertExists() + } + + /* + * Matches an element at the top of the For You destination. Should be updated when the + * destination is implemented. + */ + private fun ComposeTestRule.forYouDestinationTopMatcher() = onNodeWithText("FOR YOU") + + /* + * Matches an element at the top of the Topics destination. Should be updated when the + * destination is implemented. + */ + private fun ComposeTestRule.topicsDestinationTopMatcher() = onNodeWithText("TOPICS") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 63c4d7205..5f61d5605 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - - - - \ No newline at end of file + + diff --git a/app/src/main/res/values-v23/themes.xml b/app/src/main/res/values-v23/themes.xml new file mode 100644 index 000000000..0458654e4 --- /dev/null +++ b/app/src/main/res/values-v23/themes.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml new file mode 100644 index 000000000..7156cf11b --- /dev/null +++ b/app/src/main/res/values-v27/themes.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 24fbb2f68..a7417fcf9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,4 @@ - - + #4D000000 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3aebcfd5..269c247b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - - Now in Android - \ No newline at end of file + For you + Episodes + Saved + Topics + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 4df928334..249b3e343 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,4 @@ - - - - - - \ No newline at end of file + + + + + + + +