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