1 /*
<lambda>null2  * Copyright (C) 2023 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.customization.model.picker.color.ui.viewmodel
18 
19 import android.content.Context
20 import android.stats.style.StyleEnums
21 import androidx.test.filters.SmallTest
22 import androidx.test.platform.app.InstrumentationRegistry
23 import com.android.customization.model.color.ColorOptionsProvider
24 import com.android.customization.module.logging.TestThemesUserEventLogger
25 import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
26 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
27 import com.android.customization.picker.color.domain.interactor.ColorPickerSnapshotRestorer
28 import com.android.customization.picker.color.shared.model.ColorType
29 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
30 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
31 import com.android.customization.picker.color.ui.viewmodel.ColorTypeTabViewModel
32 import com.android.systemui.monet.Style
33 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
34 import com.android.wallpaper.testing.FakeSnapshotStore
35 import com.android.wallpaper.testing.collectLastValue
36 import com.google.common.truth.Truth.assertThat
37 import com.google.common.truth.Truth.assertWithMessage
38 import kotlinx.coroutines.Dispatchers
39 import kotlinx.coroutines.ExperimentalCoroutinesApi
40 import kotlinx.coroutines.runBlocking
41 import kotlinx.coroutines.test.StandardTestDispatcher
42 import kotlinx.coroutines.test.TestScope
43 import kotlinx.coroutines.test.advanceUntilIdle
44 import kotlinx.coroutines.test.resetMain
45 import kotlinx.coroutines.test.runTest
46 import kotlinx.coroutines.test.setMain
47 import org.junit.After
48 import org.junit.Before
49 import org.junit.Test
50 import org.junit.runner.RunWith
51 import org.robolectric.RobolectricTestRunner
52 
53 @OptIn(ExperimentalCoroutinesApi::class)
54 @SmallTest
55 @RunWith(RobolectricTestRunner::class)
56 class ColorPickerViewModelTest {
57     private val logger = TestThemesUserEventLogger()
58     private lateinit var underTest: ColorPickerViewModel
59     private lateinit var repository: FakeColorPickerRepository
60     private lateinit var interactor: ColorPickerInteractor
61     private lateinit var store: FakeSnapshotStore
62 
63     private lateinit var context: Context
64     private lateinit var testScope: TestScope
65 
66     @Before
67     fun setUp() {
68         context = InstrumentationRegistry.getInstrumentation().targetContext
69         val testDispatcher = StandardTestDispatcher()
70         Dispatchers.setMain(testDispatcher)
71         testScope = TestScope(testDispatcher)
72         repository = FakeColorPickerRepository(context = context)
73         store = FakeSnapshotStore()
74 
75         interactor =
76             ColorPickerInteractor(
77                 repository = repository,
78                 snapshotRestorer =
79                     ColorPickerSnapshotRestorer(repository = repository).apply {
80                         runBlocking { setUpSnapshotRestorer(store = store) }
81                     },
82             )
83 
84         underTest =
85             ColorPickerViewModel.Factory(
86                     context = context,
87                     interactor = interactor,
88                     logger = logger,
89                 )
90                 .create(ColorPickerViewModel::class.java)
91 
92         repository.setOptions(4, 4, ColorType.WALLPAPER_COLOR, 0)
93     }
94 
95     @After
96     fun tearDown() {
97         Dispatchers.resetMain()
98     }
99 
100     @Test
101     fun `Select a color section color`() =
102         testScope.runTest {
103             val colorSectionOptions = collectLastValue(underTest.colorSectionOptions)
104 
105             assertColorOptionUiState(
106                 colorOptions = colorSectionOptions(),
107                 selectedColorOptionIndex = 0,
108             )
109 
110             selectColorOption(colorSectionOptions, 2)
111             assertColorOptionUiState(
112                 colorOptions = colorSectionOptions(),
113                 selectedColorOptionIndex = 2,
114             )
115 
116             selectColorOption(colorSectionOptions, 4)
117             assertColorOptionUiState(
118                 colorOptions = colorSectionOptions(),
119                 selectedColorOptionIndex = 4,
120             )
121         }
122 
123     @Test
124     fun `Log selected wallpaper color`() =
125         testScope.runTest {
126             repository.setOptions(
127                 listOf(
128                     repository.buildWallpaperOption(
129                         ColorOptionsProvider.COLOR_SOURCE_LOCK,
130                         Style.EXPRESSIVE,
131                         121212,
132                     )
133                 ),
134                 listOf(repository.buildPresetOption(Style.FRUIT_SALAD, -54321)),
135                 ColorType.PRESET_COLOR,
136                 0,
137             )
138 
139             val colorTypes = collectLastValue(underTest.colorTypeTabs)
140             val colorOptions = collectLastValue(underTest.colorOptions)
141 
142             // Select "Wallpaper colors" tab
143             colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
144             // Select a color option
145             selectColorOption(colorOptions, 0)
146             advanceUntilIdle()
147 
148             assertThat(logger.themeColorSource)
149                 .isEqualTo(StyleEnums.COLOR_SOURCE_LOCK_SCREEN_WALLPAPER)
150             assertThat(logger.themeColorStyle)
151                 .isEqualTo(Style.toString(Style.EXPRESSIVE).hashCode())
152             assertThat(logger.themeSeedColor).isEqualTo(121212)
153         }
154 
155     @Test
156     fun `Log selected preset color`() =
157         testScope.runTest {
158             repository.setOptions(
159                 listOf(
160                     repository.buildWallpaperOption(
161                         ColorOptionsProvider.COLOR_SOURCE_LOCK,
162                         Style.EXPRESSIVE,
163                         121212,
164                     )
165                 ),
166                 listOf(repository.buildPresetOption(Style.FRUIT_SALAD, -54321)),
167                 ColorType.WALLPAPER_COLOR,
168                 0,
169             )
170 
171             val colorTypes = collectLastValue(underTest.colorTypeTabs)
172             val colorOptions = collectLastValue(underTest.colorOptions)
173 
174             // Select "Wallpaper colors" tab
175             colorTypes()?.get(ColorType.PRESET_COLOR)?.onClick?.invoke()
176             // Select a color option
177             selectColorOption(colorOptions, 0)
178             advanceUntilIdle()
179 
180             assertThat(logger.themeColorSource).isEqualTo(StyleEnums.COLOR_SOURCE_PRESET_COLOR)
181             assertThat(logger.themeColorStyle)
182                 .isEqualTo(Style.toString(Style.FRUIT_SALAD).hashCode())
183             assertThat(logger.themeSeedColor).isEqualTo(-54321)
184         }
185 
186     @Test
187     fun `Select a preset color`() =
188         testScope.runTest {
189             val colorTypes = collectLastValue(underTest.colorTypeTabs)
190             val colorOptions = collectLastValue(underTest.colorOptions)
191 
192             // Initially, the wallpaper color tab should be selected
193             assertPickerUiState(
194                 colorTypes = colorTypes(),
195                 colorOptions = colorOptions(),
196                 selectedColorTypeText = "Wallpaper colors",
197                 selectedColorOptionIndex = 0,
198             )
199 
200             // Select "Basic colors" tab
201             colorTypes()?.get(ColorType.PRESET_COLOR)?.onClick?.invoke()
202             assertPickerUiState(
203                 colorTypes = colorTypes(),
204                 colorOptions = colorOptions(),
205                 selectedColorTypeText = "Basic colors",
206                 selectedColorOptionIndex = -1,
207             )
208 
209             // Select a color option
210             selectColorOption(colorOptions, 2)
211 
212             // Check original option is no longer selected
213             colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
214             assertPickerUiState(
215                 colorTypes = colorTypes(),
216                 colorOptions = colorOptions(),
217                 selectedColorTypeText = "Wallpaper colors",
218                 selectedColorOptionIndex = -1,
219             )
220 
221             // Check new option is selected
222             colorTypes()?.get(ColorType.PRESET_COLOR)?.onClick?.invoke()
223             assertPickerUiState(
224                 colorTypes = colorTypes(),
225                 colorOptions = colorOptions(),
226                 selectedColorTypeText = "Basic colors",
227                 selectedColorOptionIndex = 2,
228             )
229         }
230 
231     /** Simulates a user selecting the affordance at the given index, if that is clickable. */
232     private fun TestScope.selectColorOption(
233         colorOptions: () -> List<OptionItemViewModel<ColorOptionIconViewModel>>?,
234         index: Int,
235     ) {
236         val onClickedFlow = colorOptions()?.get(index)?.onClicked
237         val onClickedLastValueOrNull: (() -> (() -> Unit)?)? =
238             onClickedFlow?.let { collectLastValue(it) }
239         onClickedLastValueOrNull?.let { onClickedLastValue ->
240             val onClickedOrNull: (() -> Unit)? = onClickedLastValue()
241             onClickedOrNull?.let { onClicked -> onClicked() }
242         }
243     }
244 
245     /**
246      * Asserts the entire picker UI state is what is expected. This includes the color type tabs and
247      * the color options list.
248      *
249      * @param colorTypes The observed color type view-models, keyed by ColorType
250      * @param colorOptions The observed color options
251      * @param selectedColorTypeText The text of the color type that's expected to be selected
252      * @param selectedColorOptionIndex The index of the color option that's expected to be selected,
253      *   -1 stands for no color option should be selected
254      */
255     private fun TestScope.assertPickerUiState(
256         colorTypes: Map<ColorType, ColorTypeTabViewModel>?,
257         colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
258         selectedColorTypeText: String,
259         selectedColorOptionIndex: Int,
260     ) {
261         assertColorTypeTabUiState(
262             colorTypes = colorTypes,
263             colorTypeId = ColorType.WALLPAPER_COLOR,
264             isSelected = "Wallpaper colors" == selectedColorTypeText,
265         )
266         assertColorTypeTabUiState(
267             colorTypes = colorTypes,
268             colorTypeId = ColorType.PRESET_COLOR,
269             isSelected = "Basic colors" == selectedColorTypeText,
270         )
271         assertColorOptionUiState(colorOptions, selectedColorOptionIndex)
272     }
273 
274     /**
275      * Asserts the picker section UI state is what is expected.
276      *
277      * @param colorOptions The observed color options
278      * @param selectedColorOptionIndex The index of the color option that's expected to be selected,
279      *   -1 stands for no color option should be selected
280      */
281     private fun TestScope.assertColorOptionUiState(
282         colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
283         selectedColorOptionIndex: Int,
284     ) {
285         var foundSelectedColorOption = false
286         assertThat(colorOptions).isNotNull()
287         if (colorOptions != null) {
288             for (i in colorOptions.indices) {
289                 val colorOptionHasSelectedIndex = i == selectedColorOptionIndex
290                 val isSelected: Boolean? = collectLastValue(colorOptions[i].isSelected).invoke()
291                 assertWithMessage(
292                         "Expected color option with index \"${i}\" to have" +
293                             " isSelected=$colorOptionHasSelectedIndex but it was" +
294                             " ${isSelected}, num options: ${colorOptions.size}"
295                     )
296                     .that(isSelected)
297                     .isEqualTo(colorOptionHasSelectedIndex)
298                 foundSelectedColorOption = foundSelectedColorOption || colorOptionHasSelectedIndex
299             }
300             if (selectedColorOptionIndex == -1) {
301                 assertWithMessage(
302                         "Expected no color options to be selected, but a color option is" +
303                             " selected"
304                     )
305                     .that(foundSelectedColorOption)
306                     .isFalse()
307             } else {
308                 assertWithMessage(
309                         "Expected a color option to be selected, but no color option is" +
310                             " selected"
311                     )
312                     .that(foundSelectedColorOption)
313                     .isTrue()
314             }
315         }
316     }
317 
318     /**
319      * Asserts that a color type tab has the correct UI state.
320      *
321      * @param colorTypes The observed color type view-models, keyed by ColorType enum
322      * @param colorTypeId the ID of the color type to assert
323      * @param isSelected Whether that color type should be selected
324      */
325     private fun assertColorTypeTabUiState(
326         colorTypes: Map<ColorType, ColorTypeTabViewModel>?,
327         colorTypeId: ColorType,
328         isSelected: Boolean,
329     ) {
330         val viewModel =
331             colorTypes?.get(colorTypeId) ?: error("No color type with ID \"$colorTypeId\"!")
332         assertThat(viewModel.isSelected).isEqualTo(isSelected)
333     }
334 }
335