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