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