1 /*
2  * Copyright (C) 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.features.overflowmenu
18 
19 import android.content.ContentResolver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.pm.PackageManager
23 import android.os.UserManager
24 import android.provider.MediaStore
25 import android.test.mock.MockContentResolver
26 import androidx.compose.runtime.CompositionLocalProvider
27 import androidx.compose.ui.test.ExperimentalTestApi
28 import androidx.compose.ui.test.assert
29 import androidx.compose.ui.test.assertIsDisplayed
30 import androidx.compose.ui.test.assertIsNotDisplayed
31 import androidx.compose.ui.test.hasClickAction
32 import androidx.compose.ui.test.hasContentDescription
33 import androidx.compose.ui.test.hasText
34 import androidx.compose.ui.test.junit4.createAndroidComposeRule
35 import androidx.compose.ui.test.performClick
36 import com.android.photopicker.R
37 import com.android.photopicker.core.ActivityModule
38 import com.android.photopicker.core.ApplicationModule
39 import com.android.photopicker.core.ApplicationOwned
40 import com.android.photopicker.core.Background
41 import com.android.photopicker.core.ConcurrencyModule
42 import com.android.photopicker.core.EmbeddedServiceModule
43 import com.android.photopicker.core.Main
44 import com.android.photopicker.core.configuration.ConfigurationManager
45 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
46 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
47 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
48 import com.android.photopicker.core.events.Events
49 import com.android.photopicker.core.events.LocalEvents
50 import com.android.photopicker.core.events.RegisteredEventClass
51 import com.android.photopicker.core.features.FeatureManager
52 import com.android.photopicker.core.features.FeatureToken.OVERFLOW_MENU
53 import com.android.photopicker.core.features.LocalFeatureManager
54 import com.android.photopicker.core.features.Location
55 import com.android.photopicker.core.glide.GlideTestRule
56 import com.android.photopicker.data.TestPrefetchDataService
57 import com.android.photopicker.features.PhotopickerFeatureBaseTest
58 import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
59 import com.android.photopicker.inject.PhotopickerTestModule
60 import com.android.photopicker.tests.HiltTestActivity
61 import com.google.common.truth.Truth.assertWithMessage
62 import dagger.Lazy
63 import dagger.Module
64 import dagger.hilt.InstallIn
65 import dagger.hilt.android.testing.BindValue
66 import dagger.hilt.android.testing.HiltAndroidRule
67 import dagger.hilt.android.testing.HiltAndroidTest
68 import dagger.hilt.android.testing.UninstallModules
69 import dagger.hilt.components.SingletonComponent
70 import javax.inject.Inject
71 import kotlinx.coroutines.CoroutineDispatcher
72 import kotlinx.coroutines.CoroutineScope
73 import kotlinx.coroutines.ExperimentalCoroutinesApi
74 import kotlinx.coroutines.test.StandardTestDispatcher
75 import kotlinx.coroutines.test.TestScope
76 import kotlinx.coroutines.test.advanceTimeBy
77 import kotlinx.coroutines.test.runTest
78 import org.junit.Before
79 import org.junit.Rule
80 import org.junit.Test
81 import org.mockito.Mock
82 import org.mockito.MockitoAnnotations
83 
84 @UninstallModules(
85     ActivityModule::class,
86     ApplicationModule::class,
87     ConcurrencyModule::class,
88     EmbeddedServiceModule::class,
89 )
90 @HiltAndroidTest
91 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
92 class OverflowMenuFeatureTest : PhotopickerFeatureBaseTest() {
93 
94     /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
95     @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
96     @get:Rule(order = 1)
97     val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
98     @get:Rule(order = 2) val glideRule = GlideTestRule()
99 
100     /* Setup dependencies for the UninstallModules for the test class. */
101     @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
102 
103     val testDispatcher = StandardTestDispatcher()
104 
105     /* Overrides for ActivityModule */
106     val testScope: TestScope = TestScope(testDispatcher)
107     @BindValue @Main val mainScope: CoroutineScope = testScope
108     @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
109 
110     /* Overrides for the ConcurrencyModule */
111     @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
112     @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
113 
114     @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
115 
116     // Needed for UserMonitor
117     @Inject lateinit var mockContext: Context
118     @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
119     @Mock lateinit var mockUserManager: UserManager
120     @Mock lateinit var mockPackageManager: PackageManager
121 
122     @Before
setupnull123     fun setup() {
124         MockitoAnnotations.initMocks(this)
125         hiltRule.inject()
126         setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
127     }
128 
129     @Test
testOverflowMenuEnabledInConfigurationsnull130     fun testOverflowMenuEnabledInConfigurations() {
131 
132         assertWithMessage("OverflowMenuFeature is not always enabled (ACTION_PICK_IMAGES)")
133             .that(
134                 OverflowMenuFeature.Registration.isEnabled(
135                     TestPhotopickerConfiguration.build {
136                         action(MediaStore.ACTION_PICK_IMAGES)
137                         intent(Intent(MediaStore.ACTION_PICK_IMAGES))
138                     }
139                 )
140             )
141             .isEqualTo(true)
142 
143         assertWithMessage("OverflowMenuFeature is not always enabled (ACTION_GET_CONTENT)")
144             .that(
145                 OverflowMenuFeature.Registration.isEnabled(
146                     TestPhotopickerConfiguration.build {
147                         action(Intent.ACTION_GET_CONTENT)
148                         intent(Intent(Intent.ACTION_GET_CONTENT))
149                     }
150                 )
151             )
152             .isEqualTo(true)
153 
154         assertWithMessage("OverflowMenuFeature is not always enabled (USER_SELECT_FOR_APP)")
155             .that(
156                 OverflowMenuFeature.Registration.isEnabled(
157                     TestPhotopickerConfiguration.build {
158                         action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
159                         intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
160                         callingPackage("com.example.test")
161                         callingPackageUid(1234)
162                         callingPackageLabel("test_app")
163                     }
164                 )
165             )
166             .isEqualTo(true)
167     }
168 
169     @Test
testOverflowMenuAnchorShownIfMenuItemsExistnull170     fun testOverflowMenuAnchorShownIfMenuItemsExist() =
171         testScope.runTest {
172             val featureManager =
173                 FeatureManager(
174                     provideTestConfigurationFlow(scope = this.backgroundScope),
175                     this.backgroundScope,
176                     TestPrefetchDataService(),
177                     setOf(SimpleUiFeature.Registration, OverflowMenuFeature.Registration),
178                     /*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
179                     /*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
180                 )
181 
182             val events =
183                 Events(
184                     this.backgroundScope,
185                     provideTestConfigurationFlow(scope = this.backgroundScope),
186                     featureManager,
187                 )
188 
189             composeTestRule.setContent {
190                 CompositionLocalProvider(
191                     LocalFeatureManager provides featureManager,
192                     LocalEvents provides events,
193                     LocalPhotopickerConfiguration provides
194                         TestPhotopickerConfiguration.build {
195                             action("TEST_ACTION")
196                             intent(Intent("TEST_ACTION"))
197                         },
198                 ) {
199                     featureManager.composeLocation(Location.OVERFLOW_MENU)
200                 }
201             }
202             composeTestRule
203                 .onNode(
204                     hasContentDescription(
205                         getTestableContext()
206                             .getResources()
207                             .getString(R.string.photopicker_overflow_menu_description)
208                     )
209                 )
210                 .assert(hasClickAction())
211                 .assertIsDisplayed()
212         }
213 
214     @Test
testOverflowMenuAnchorHiddenWhenNoMenuItemsExistnull215     fun testOverflowMenuAnchorHiddenWhenNoMenuItemsExist() =
216         testScope.runTest {
217             val featureManager =
218                 FeatureManager(
219                     provideTestConfigurationFlow(scope = this.backgroundScope),
220                     this.backgroundScope,
221                     TestPrefetchDataService(),
222                     setOf(OverflowMenuFeature.Registration),
223                     /*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
224                     /*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
225                 )
226 
227             val events =
228                 Events(
229                     this.backgroundScope,
230                     provideTestConfigurationFlow(scope = this.backgroundScope),
231                     featureManager,
232                 )
233 
234             composeTestRule.setContent {
235                 CompositionLocalProvider(
236                     LocalFeatureManager provides featureManager,
237                     LocalEvents provides events,
238                 ) {
239                     featureManager.composeLocation(Location.OVERFLOW_MENU)
240                 }
241             }
242             composeTestRule
243                 .onNode(
244                     hasContentDescription(
245                         getTestableContext()
246                             .getResources()
247                             .getString(R.string.photopicker_overflow_menu_description)
248                     )
249                 )
250                 .assertIsNotDisplayed()
251         }
252 
253     @Test
testOverflowMenuIsHiddenAfterItemSelectednull254     fun testOverflowMenuIsHiddenAfterItemSelected() =
255         testScope.runTest {
256             val featureManager =
257                 FeatureManager(
258                     provideTestConfigurationFlow(scope = this.backgroundScope),
259                     this.backgroundScope,
260                     TestPrefetchDataService(),
261                     setOf(SimpleUiFeature.Registration, OverflowMenuFeature.Registration),
262                     /*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
263                     /*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
264                 )
265             val events =
266                 Events(
267                     this.backgroundScope,
268                     provideTestConfigurationFlow(scope = this.backgroundScope),
269                     featureManager,
270                 )
271 
272             composeTestRule.setContent {
273                 CompositionLocalProvider(
274                     LocalFeatureManager provides featureManager,
275                     LocalEvents provides events,
276                     LocalPhotopickerConfiguration provides
277                         TestPhotopickerConfiguration.build {
278                             action("TEST_ACTION")
279                             intent(Intent("TEST_ACTION"))
280                         },
281                 ) {
282                     featureManager.composeLocation(Location.OVERFLOW_MENU)
283                 }
284             }
285             composeTestRule
286                 .onNode(
287                     hasContentDescription(
288                         getTestableContext()
289                             .getResources()
290                             .getString(R.string.photopicker_overflow_menu_description)
291                     )
292                 )
293                 .performClick()
294 
295             composeTestRule
296                 .onNode(hasText(SimpleUiFeature.Registration.BUTTON_LABEL))
297                 .assertIsDisplayed()
298                 .performClick()
299 
300             advanceTimeBy(100)
301 
302             composeTestRule
303                 .onNode(hasText(SimpleUiFeature.Registration.BUTTON_LABEL))
304                 .assertIsNotDisplayed()
305         }
306 }
307