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.features.snackbar 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.assertIsDisplayed 29 import androidx.compose.ui.test.assertIsNotDisplayed 30 import androidx.compose.ui.test.hasText 31 import androidx.compose.ui.test.junit4.createAndroidComposeRule 32 import com.android.photopicker.core.ActivityModule 33 import com.android.photopicker.core.ApplicationModule 34 import com.android.photopicker.core.ApplicationOwned 35 import com.android.photopicker.core.Background 36 import com.android.photopicker.core.ConcurrencyModule 37 import com.android.photopicker.core.EmbeddedServiceModule 38 import com.android.photopicker.core.Main 39 import com.android.photopicker.core.configuration.ConfigurationManager 40 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration 41 import com.android.photopicker.core.events.Event 42 import com.android.photopicker.core.events.Events 43 import com.android.photopicker.core.events.LocalEvents 44 import com.android.photopicker.core.features.FeatureManager 45 import com.android.photopicker.core.features.FeatureToken.CORE 46 import com.android.photopicker.core.features.LocalFeatureManager 47 import com.android.photopicker.core.features.Location 48 import com.android.photopicker.core.glide.GlideTestRule 49 import com.android.photopicker.core.navigation.LocalNavController 50 import com.android.photopicker.core.selection.LocalSelection 51 import com.android.photopicker.core.selection.Selection 52 import com.android.photopicker.data.model.Media 53 import com.android.photopicker.features.PhotopickerFeatureBaseTest 54 import com.android.photopicker.inject.PhotopickerTestModule 55 import com.android.photopicker.tests.HiltTestActivity 56 import com.android.photopicker.util.test.whenever 57 import com.google.common.truth.Truth.assertWithMessage 58 import dagger.Lazy 59 import dagger.Module 60 import dagger.hilt.InstallIn 61 import dagger.hilt.android.testing.BindValue 62 import dagger.hilt.android.testing.HiltAndroidRule 63 import dagger.hilt.android.testing.HiltAndroidTest 64 import dagger.hilt.android.testing.UninstallModules 65 import dagger.hilt.components.SingletonComponent 66 import javax.inject.Inject 67 import kotlinx.coroutines.CoroutineDispatcher 68 import kotlinx.coroutines.CoroutineScope 69 import kotlinx.coroutines.ExperimentalCoroutinesApi 70 import kotlinx.coroutines.test.StandardTestDispatcher 71 import kotlinx.coroutines.test.TestScope 72 import kotlinx.coroutines.test.advanceTimeBy 73 import kotlinx.coroutines.test.runTest 74 import org.junit.Before 75 import org.junit.Rule 76 import org.junit.Test 77 import org.mockito.Mock 78 import org.mockito.MockitoAnnotations 79 80 @UninstallModules( 81 ActivityModule::class, 82 ApplicationModule::class, 83 ConcurrencyModule::class, 84 EmbeddedServiceModule::class, 85 ) 86 @HiltAndroidTest 87 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) 88 class SnackbarFeatureTest : PhotopickerFeatureBaseTest() { 89 90 /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */ 91 @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) 92 @get:Rule(order = 1) 93 val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) 94 @get:Rule(order = 2) val glideRule = GlideTestRule() 95 96 /* Setup dependencies for the UninstallModules for the test class. */ 97 @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() 98 99 val testDispatcher = StandardTestDispatcher() 100 101 /* Overrides for ActivityModule */ 102 val testScope: TestScope = TestScope(testDispatcher) 103 @BindValue @Main val mainScope: CoroutineScope = testScope 104 @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope 105 106 /* Overrides for the ConcurrencyModule */ 107 @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher 108 @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher 109 110 @Mock lateinit var mockUserManager: UserManager 111 @Mock lateinit var mockPackageManager: PackageManager 112 113 @BindValue @ApplicationOwned lateinit var mockContentResolver: ContentResolver 114 115 @Inject lateinit var mockContext: Context 116 @Inject lateinit var selection: Selection<Media> 117 @Inject lateinit var featureManager: FeatureManager 118 @Inject override lateinit var configurationManager: Lazy<ConfigurationManager> 119 @Inject lateinit var events: Events 120 121 @Before setupnull122 fun setup() { 123 MockitoAnnotations.initMocks(this) 124 hiltRule.inject() 125 126 // Stub for MockContentResolver constructor 127 whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() } 128 mockContentResolver = MockContentResolver(mockContext) 129 130 setupTestForUserMonitor( 131 mockContext, 132 mockUserManager, 133 mockContentResolver, 134 mockPackageManager, 135 ) 136 } 137 138 @Test testSnackbarIsAlwaysEnablednull139 fun testSnackbarIsAlwaysEnabled() { 140 141 assertWithMessage("SnackbarFeature is not always enabled for action pick image") 142 .that( 143 SnackbarFeature.Registration.isEnabled( 144 TestPhotopickerConfiguration.build { 145 action(MediaStore.ACTION_PICK_IMAGES) 146 intent(Intent(MediaStore.ACTION_PICK_IMAGES)) 147 } 148 ) 149 ) 150 .isEqualTo(true) 151 152 assertWithMessage("SnackbarFeature is not always enabled for get content") 153 .that( 154 SnackbarFeature.Registration.isEnabled( 155 TestPhotopickerConfiguration.build { 156 action(Intent.ACTION_GET_CONTENT) 157 intent(Intent(Intent.ACTION_GET_CONTENT)) 158 } 159 ) 160 ) 161 .isEqualTo(true) 162 163 assertWithMessage("SnackbarFeature is not always enabled for user select images") 164 .that( 165 SnackbarFeature.Registration.isEnabled( 166 TestPhotopickerConfiguration.build { 167 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) 168 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) 169 callingPackage("com.example.test") 170 callingPackageUid(1234) 171 callingPackageLabel("test_app") 172 } 173 ) 174 ) 175 .isEqualTo(true) 176 } 177 178 @Test testSnackbarDisplaysOnEventnull179 fun testSnackbarDisplaysOnEvent() = 180 testScope.runTest { 181 composeTestRule.setContent { 182 CompositionLocalProvider( 183 LocalFeatureManager provides featureManager, 184 LocalSelection provides selection, 185 LocalEvents provides events, 186 LocalNavController provides createNavController(), 187 ) { 188 LocalFeatureManager.current.composeLocation(Location.SNACK_BAR, maxSlots = 1) 189 } 190 } 191 192 // Advance the UI clock manually to control for the fade animations on the snackbar. 193 composeTestRule.mainClock.autoAdvance = false 194 195 val TEST_MESSAGE = "This is a test message" 196 events.dispatch(Event.ShowSnackbarMessage(CORE.token, TEST_MESSAGE)) 197 advanceTimeBy(500) 198 199 // Advance ui clock to allow fade in 200 composeTestRule.mainClock.advanceTimeBy(2000L) 201 composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsDisplayed() 202 203 // Advance ui clock to allow fade out 204 composeTestRule.mainClock.advanceTimeBy(10_000L) 205 composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsNotDisplayed() 206 } 207 } 208