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