1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.photopicker.core.navigation 18 19 import androidx.compose.runtime.Composable 20 import androidx.compose.runtime.CompositionLocalProvider 21 import androidx.compose.ui.platform.LocalContext 22 import androidx.compose.ui.test.assertIsDisplayed 23 import androidx.compose.ui.test.junit4.createComposeRule 24 import androidx.compose.ui.test.onNodeWithText 25 import androidx.compose.ui.test.performClick 26 import androidx.navigation.compose.ComposeNavigator 27 import androidx.navigation.compose.DialogNavigator 28 import androidx.navigation.testing.TestNavHostController 29 import androidx.test.ext.junit.runners.AndroidJUnit4 30 import androidx.test.filters.SmallTest 31 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration 32 import com.android.photopicker.core.configuration.PhotopickerConfiguration 33 import com.android.photopicker.core.configuration.provideTestConfigurationFlow 34 import com.android.photopicker.core.events.RegisteredEventClass 35 import com.android.photopicker.core.events.generatePickerSessionId 36 import com.android.photopicker.core.features.FeatureManager 37 import com.android.photopicker.core.features.FeatureRegistration 38 import com.android.photopicker.core.features.LocalFeatureManager 39 import com.android.photopicker.data.TestPrefetchDataService 40 import com.android.photopicker.features.alwaysdisabledfeature.AlwaysDisabledFeature 41 import com.android.photopicker.features.highpriorityuifeature.HighPriorityUiFeature 42 import com.android.photopicker.features.simpleuifeature.SimpleUiFeature 43 import com.google.common.truth.Truth.assertThat 44 import kotlinx.coroutines.ExperimentalCoroutinesApi 45 import kotlinx.coroutines.test.TestScope 46 import org.junit.Before 47 import org.junit.Rule 48 import org.junit.Test 49 import org.junit.runner.RunWith 50 51 /** Unit tests for the [PhotopickerNavGraph] composable. */ 52 @SmallTest 53 @RunWith(AndroidJUnit4::class) 54 @OptIn(ExperimentalCoroutinesApi::class) 55 class PhotopickerNavGraphTest { 56 57 @get:Rule val composeTestRule = createComposeRule() 58 59 lateinit var navController: TestNavHostController 60 lateinit var featureManager: FeatureManager 61 private val sessionId = generatePickerSessionId() 62 63 val testRegistrations = 64 setOf( 65 SimpleUiFeature.Registration, 66 HighPriorityUiFeature.Registration, 67 AlwaysDisabledFeature.Registration, 68 ) 69 70 @Before setupnull71 fun setup() { 72 73 // Initialize a basic [FeatureManager] with the standard test registrations. 74 val scope = TestScope() 75 featureManager = 76 FeatureManager( 77 provideTestConfigurationFlow(scope = scope.backgroundScope), 78 scope, 79 TestPrefetchDataService(), 80 testRegistrations, 81 /*coreEventsConsumed=*/ setOf<RegisteredEventClass>(), 82 /*coreEventsProduced=*/ setOf<RegisteredEventClass>(), 83 ) 84 } 85 86 /** 87 * Composable that creates a new [TestNavHostController] and wraps the [PhotopickerNavGraph] 88 * with its expected providers 89 */ 90 @Composable testNavGraphnull91 private fun testNavGraph( 92 featureManager: FeatureManager, 93 configuration: PhotopickerConfiguration = 94 PhotopickerConfiguration(action = "", sessionId = sessionId), 95 ) { 96 navController = TestNavHostController(LocalContext.current) 97 navController.navigatorProvider.addNavigator(ComposeNavigator()) 98 navController.navigatorProvider.addNavigator(DialogNavigator()) 99 // Provide the feature manager to the compose stack. 100 CompositionLocalProvider( 101 LocalPhotopickerConfiguration provides configuration, 102 LocalFeatureManager provides featureManager, 103 ) { 104 105 // Provide the nav controller via [CompositionLocalProvider] to 106 // simulate how it receives it at runtime. 107 CompositionLocalProvider(LocalNavController provides navController) { 108 PhotopickerNavGraph() 109 } 110 } 111 } 112 113 /** 114 * Ensures that if no features are enabled in [FeatureManager], we always have a default route 115 * to prevent the UI from crashing 116 */ 117 @Test testVerifyStartDestinationWithNoFeaturesnull118 fun testVerifyStartDestinationWithNoFeatures() { 119 120 val scope = TestScope() 121 val emptyFeatureManager = 122 FeatureManager( 123 provideTestConfigurationFlow(scope = scope.backgroundScope), 124 scope, 125 TestPrefetchDataService(), 126 emptySet<FeatureRegistration>(), 127 /*coreEventsConsumed=*/ setOf<RegisteredEventClass>(), 128 /*coreEventsProduced=*/ setOf<RegisteredEventClass>(), 129 ) 130 131 composeTestRule.setContent { testNavGraph(emptyFeatureManager) } 132 133 val route = navController.currentBackStackEntry?.destination?.route 134 assertThat(route).isEqualTo(PhotopickerDestinations.DEFAULT.route) 135 } 136 137 /** 138 * Ensures that the enabled feature route with the highest priority is set as the default 139 * starting route. 140 */ 141 @Test testStartDestinationWithPrioritiesnull142 fun testStartDestinationWithPriorities() { 143 144 composeTestRule.setContent { testNavGraph(featureManager) } 145 146 val route = navController.currentBackStackEntry?.destination?.route 147 assertThat(route).isEqualTo(HighPriorityUiFeature.START_ROUTE) 148 composeTestRule.onNodeWithText(HighPriorityUiFeature.START_STRING).assertIsDisplayed() 149 composeTestRule.onNodeWithText(HighPriorityUiFeature.DIALOG_STRING).assertDoesNotExist() 150 } 151 152 /** Ensures that the starting route passed in the configuration is chosen, if available. */ 153 @Test testStartDestinationWithAlbumGridConfigurationnull154 fun testStartDestinationWithAlbumGridConfiguration() { 155 156 val config = 157 PhotopickerConfiguration( 158 action = "", 159 startDestination = PhotopickerDestinations.ALBUM_GRID, 160 sessionId = sessionId, 161 ) 162 composeTestRule.setContent { testNavGraph(featureManager, config) } 163 164 val route = navController.currentBackStackEntry?.destination?.route 165 assertThat(route).isEqualTo(PhotopickerDestinations.ALBUM_GRID.route) 166 } 167 168 /** Ensures that the starting route passed in the configuration is chosen, if available. */ 169 @Test testStartDestinationWithPhotoGridConfigurationnull170 fun testStartDestinationWithPhotoGridConfiguration() { 171 172 val config = 173 PhotopickerConfiguration( 174 action = "", 175 startDestination = PhotopickerDestinations.PHOTO_GRID, 176 sessionId = sessionId, 177 ) 178 composeTestRule.setContent { testNavGraph(featureManager, config) } 179 180 val route = navController.currentBackStackEntry?.destination?.route 181 assertThat(route).isEqualTo(PhotopickerDestinations.PHOTO_GRID.route) 182 } 183 184 /** Ensures that composables can navigate to dialogs on the graph. */ 185 @Test testNavigationGraphIsNavigableToDialogsnull186 fun testNavigationGraphIsNavigableToDialogs() { 187 188 composeTestRule.setContent { testNavGraph(featureManager) } 189 190 // Start route is decided based on priority. 191 var route = navController.currentBackStackEntry?.destination?.route 192 assertThat(route).isEqualTo(HighPriorityUiFeature.START_ROUTE) 193 composeTestRule.onNodeWithText(HighPriorityUiFeature.START_STRING).assertIsDisplayed() 194 composeTestRule.onNodeWithText(HighPriorityUiFeature.DIALOG_STRING).assertDoesNotExist() 195 composeTestRule.onNodeWithText("navigate to dialog").performClick() 196 197 // After clicking the button it should be on the dialog route now 198 route = navController.currentBackStackEntry?.destination?.route 199 assertThat(route).isEqualTo(HighPriorityUiFeature.DIALOG_ROUTE) 200 composeTestRule.onNodeWithText(HighPriorityUiFeature.DIALOG_STRING).assertIsDisplayed() 201 composeTestRule.onNodeWithText("navigate to start").performClick() 202 203 // After clicking the button it should be on the start route now 204 route = navController.currentBackStackEntry?.destination?.route 205 assertThat(route).isEqualTo(HighPriorityUiFeature.START_ROUTE) 206 composeTestRule.onNodeWithText(HighPriorityUiFeature.START_STRING).assertIsDisplayed() 207 composeTestRule.onNodeWithText(HighPriorityUiFeature.DIALOG_STRING).assertDoesNotExist() 208 } 209 210 /** Ensures that composables can navigate between routes on the graph. */ 211 @Test testNavigationGraphIsNavigableToFeatureRoutesnull212 fun testNavigationGraphIsNavigableToFeatureRoutes() { 213 214 composeTestRule.setContent { testNavGraph(featureManager) } 215 216 var route = navController.currentBackStackEntry?.destination?.route 217 assertThat(route).isEqualTo(HighPriorityUiFeature.START_ROUTE) 218 composeTestRule.onNodeWithText(HighPriorityUiFeature.START_STRING).assertIsDisplayed() 219 composeTestRule.onNodeWithText(SimpleUiFeature.UI_STRING).assertDoesNotExist() 220 composeTestRule.onNodeWithText("navigate to simple ui").performClick() 221 222 route = navController.currentBackStackEntry?.destination?.route 223 224 assertThat(route).isEqualTo(SimpleUiFeature.SIMPLE_ROUTE) 225 composeTestRule.onNodeWithText(HighPriorityUiFeature.START_STRING).assertDoesNotExist() 226 composeTestRule.onNodeWithText(SimpleUiFeature.UI_STRING).assertIsDisplayed() 227 228 // Simulate a back press to navigate back to the previous route. 229 composeTestRule.runOnUiThread { navController.popBackStack() } 230 route = navController.currentBackStackEntry?.destination?.route 231 assertThat(route).isEqualTo(HighPriorityUiFeature.START_ROUTE) 232 } 233 } 234