diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3d4a9af2..cff4ce84c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,8 +98,10 @@ dependencies { androidTestImplementation(project(":core:data-test")) androidTestImplementation(project(":core:network")) androidTestImplementation(libs.androidx.navigation.testing) + androidTestImplementation(libs.accompanist.testharness) androidTestImplementation(kotlin("test")) debugImplementation(libs.androidx.compose.ui.testManifest) + debugImplementation(project(":ui-test-hilt-manifest")) implementation(libs.accompanist.systemuicontroller) implementation(libs.androidx.activity.compose) diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt new file mode 100644 index 000000000..40b7a6c29 --- /dev/null +++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt @@ -0,0 +1,243 @@ +/* + * 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.compose.foundation.layout.BoxWithConstraints +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.google.accompanist.testharness.TestHarness +import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor +import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import javax.inject.Inject +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +/** + * Tests that the navigation UI is rendered correctly on different screen sizes. + */ +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@HiltAndroidTest +class NavigationUiTest { + + /** + * 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 a test activity to set the content on. + */ + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Before + fun setup() { + hiltRule.inject() + } + + @Test + fun compactWidth_compactHeight_showsNavigationBar() { + composeTestRule.setContent { + TestHarness(size = DpSize(400.dp, 400.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() + } + + @Test + fun mediumWidth_compactHeight_showsNavigationBar() { + composeTestRule.setContent { + TestHarness(size = DpSize(610.dp, 400.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() + } + + @Test + fun expandedWidth_compactHeight_showsNavigationBar() { + composeTestRule.setContent { + TestHarness(size = DpSize(900.dp, 400.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() + } + + @Test + fun compcatWidth_mediumHeight_showsNavigationBar() { + composeTestRule.setContent { + TestHarness(size = DpSize(400.dp, 500.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() + } + + @Test + fun mediumWidth_mediumHeight_showsNavigationRail() { + composeTestRule.setContent { + TestHarness(size = DpSize(610.dp, 500.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() + } + + @Test + fun expandedWidth_mediumHeight_showsNavigationRail() { + composeTestRule.setContent { + TestHarness(size = DpSize(900.dp, 500.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() + } + + @Test + fun compactWidth_expandedHeight_showsNavigationBar() { + composeTestRule.setContent { + TestHarness(size = DpSize(400.dp, 1000.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() + } + + @Test + fun mediumWidth_expandedHeight_showsNavigationRail() { + composeTestRule.setContent { + TestHarness(size = DpSize(610.dp, 1000.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() + } + + @Test + fun expandedWidth_expandedHeight_showsNavigationRail() { + composeTestRule.setContent { + TestHarness(size = DpSize(900.dp, 1000.dp)) { + BoxWithConstraints { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight) + ), + networkMonitor = networkMonitor + ) + } + } + } + + composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() + composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() + } +} diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 30acd84cf..26262e53f 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -44,6 +44,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi 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.semantics @@ -133,7 +134,8 @@ fun NiaApp( NiaBottomBar( destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination + currentDestination = appState.currentDestination, + modifier = Modifier.testTag("NiaBottomBar") ) } } @@ -170,7 +172,9 @@ fun NiaApp( destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, - modifier = Modifier.safeDrawingPadding() + modifier = Modifier + .testTag("NiaNavRail") + .safeDrawingPadding() ) } @@ -230,9 +234,12 @@ private fun NiaNavRail( private fun NiaBottomBar( destinations: List, onNavigateToDestination: (TopLevelDestination) -> Unit, - currentDestination: NavDestination? + currentDestination: NavDestination?, + modifier: Modifier = Modifier ) { - NiaNavigationBar { + NiaNavigationBar( + modifier = modifier + ) { destinations.forEach { destination -> val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) NiaNavigationBarItem( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5892e277d..401bcefec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -accompanist = "0.27.0" +accompanist = "0.28.0" androidDesugarJdkLibs = "1.2.0" androidGradlePlugin = "7.3.1" androidxActivity = "1.6.1" @@ -49,6 +49,7 @@ turbine = "0.12.1" [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } +accompanist-testharness = { group = "com.google.accompanist", name = "accompanist-testharness", version.ref = "accompanist" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } diff --git a/settings.gradle.kts b/settings.gradle.kts index cfa31a150..295cedf36 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,3 +55,4 @@ include(":feature:settings") include(":lint") include(":sync:work") include(":sync:sync-test") +include(":ui-test-hilt-manifest") diff --git a/ui-test-hilt-manifest/.gitignore b/ui-test-hilt-manifest/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/ui-test-hilt-manifest/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ui-test-hilt-manifest/build.gradle.kts b/ui-test-hilt-manifest/build.gradle.kts new file mode 100644 index 000000000..1850f82c2 --- /dev/null +++ b/ui-test-hilt-manifest/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * 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. + */ +plugins { + id("nowinandroid.android.library") + kotlin("kapt") + id("dagger.hilt.android.plugin") +} + +dependencies { + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) +} diff --git a/ui-test-hilt-manifest/src/main/AndroidManifest.xml b/ui-test-hilt-manifest/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4b97b92be --- /dev/null +++ b/ui-test-hilt-manifest/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui-test-hilt-manifest/src/main/java/com/google/samples/apps/nowinandroid/uitesthiltmanifest/HiltComponentActivity.kt b/ui-test-hilt-manifest/src/main/java/com/google/samples/apps/nowinandroid/uitesthiltmanifest/HiltComponentActivity.kt new file mode 100644 index 000000000..dae9bca82 --- /dev/null +++ b/ui-test-hilt-manifest/src/main/java/com/google/samples/apps/nowinandroid/uitesthiltmanifest/HiltComponentActivity.kt @@ -0,0 +1,27 @@ +/* + * 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.uitesthiltmanifest + +import androidx.activity.ComponentActivity +import dagger.hilt.android.AndroidEntryPoint + +/** + * A [ComponentActivity] annotated with [AndroidEntryPoint] for use in tests, as a workaround + * for https://github.com/google/dagger/issues/3394 + */ +@AndroidEntryPoint +class HiltComponentActivity : ComponentActivity()