From 369cd33b7ae8fe127f899513cfd6fd21e5f3c8b9 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 28 Jul 2022 16:51:59 -0700 Subject: [PATCH] Add accompanist/testharness Adds a usage of accompanist/testharnness to force a given DpSize for its content, overriding the density if necessary to ensure that the size is achieved. This allows testing UI at arbitrary sizes on arbitrary emulators, lessening the need for using multiple emulators at different sizes (and filtering tests) when Android-specific window size has been hoisted appropriately. The initial test using it is a test for checking whether or not the navigation bar or navigation rail is being shown, and these tests can run on any emulator. Change-Id: I315384e5eafac23a3cd6d3818d9828f6d3c1acfc --- app/build.gradle.kts | 2 + .../apps/nowinandroid/ui/NavigationUiTest.kt | 243 ++++++++++++++++++ .../samples/apps/nowinandroid/ui/NiaApp.kt | 15 +- gradle/libs.versions.toml | 3 +- settings.gradle.kts | 1 + ui-test-hilt-manifest/.gitignore | 1 + ui-test-hilt-manifest/build.gradle.kts | 25 ++ .../src/main/AndroidManifest.xml | 24 ++ .../HiltComponentActivity.kt | 27 ++ 9 files changed, 336 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt create mode 100644 ui-test-hilt-manifest/.gitignore create mode 100644 ui-test-hilt-manifest/build.gradle.kts create mode 100644 ui-test-hilt-manifest/src/main/AndroidManifest.xml create mode 100644 ui-test-hilt-manifest/src/main/java/com/google/samples/apps/nowinandroid/uitesthiltmanifest/HiltComponentActivity.kt 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..d7eb2d9f9 --- /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 org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import javax.inject.Inject + +/** + * 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()