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.core.selection 18 19 import androidx.test.ext.junit.runners.AndroidJUnit4 20 import androidx.test.filters.SmallTest 21 import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG 22 import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG 23 import com.android.photopicker.core.configuration.provideTestConfigurationFlow 24 import com.android.photopicker.data.model.Grantable 25 import com.google.common.truth.Truth.assertWithMessage 26 import kotlinx.coroutines.ExperimentalCoroutinesApi 27 import kotlinx.coroutines.flow.first 28 import kotlinx.coroutines.flow.toList 29 import kotlinx.coroutines.launch 30 import kotlinx.coroutines.test.advanceTimeBy 31 import kotlinx.coroutines.test.runTest 32 import org.junit.Test 33 import org.junit.runner.RunWith 34 35 @SmallTest 36 @RunWith(AndroidJUnit4::class) 37 @OptIn(ExperimentalCoroutinesApi::class) 38 class GrantsAwareSelectionTest { 39 40 /** A sample data class used only for testing. */ 41 private data class SelectionData(val id: Int, val isPreGrantedParam: Boolean = false) : 42 Grantable { 43 override val isPreGranted: Boolean = isPreGrantedParam 44 } 45 46 private val INITIAL_SELECTION = <lambda>null47 buildSet<SelectionData> { 48 for (i in 1..10) { 49 add(SelectionData(id = i)) 50 } 51 } 52 53 /** Ensures the selection is initialized as empty when no items are provided. */ 54 @Test <lambda>null55 fun testSelectionIsEmptyByDefault() = runTest { 56 val selection: Selection<SelectionData> = 57 GrantsAwareSelectionImpl( 58 scope = backgroundScope, 59 configuration = 60 provideTestConfigurationFlow( 61 scope = backgroundScope, 62 defaultConfiguration = SINGLE_SELECT_CONFIG 63 ) 64 ) 65 val snapshot = selection.snapshot() 66 67 assertWithMessage("Snapshot was expected to be empty.").that(snapshot).isEmpty() 68 assertWithMessage("Emitted flow was expected to be empty.") 69 .that(selection.flow.first()) 70 .isEmpty() 71 } 72 73 /** Ensures the selection is initialized with the provided items. */ 74 @Test testSelectionIsInitializednull75 fun testSelectionIsInitialized() = runTest { 76 val selection: Selection<SelectionData> = 77 GrantsAwareSelectionImpl( 78 scope = backgroundScope, 79 configuration = 80 provideTestConfigurationFlow( 81 scope = backgroundScope, 82 defaultConfiguration = MULTI_SELECT_CONFIG 83 ), 84 initialSelection = INITIAL_SELECTION 85 ) 86 87 val snapshot = selection.snapshot() 88 val flow = selection.flow.first() 89 90 assertWithMessage("Snapshot was expected to contain the initial selection") 91 .that(snapshot) 92 .isEqualTo(INITIAL_SELECTION) 93 assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(10) 94 95 assertWithMessage("Emitted flow was expected to contain the initial selection") 96 .that(flow) 97 .isEqualTo(INITIAL_SELECTION) 98 assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(10) 99 } 100 101 @Test <lambda>null102 fun testSelectionReturnsSuccess() = runTest { 103 val selection: Selection<SelectionData> = 104 GrantsAwareSelectionImpl( 105 scope = backgroundScope, 106 configuration = 107 provideTestConfigurationFlow( 108 scope = backgroundScope, 109 defaultConfiguration = MULTI_SELECT_CONFIG 110 ), 111 ) 112 113 114 assertWithMessage("Selection addition was expected to be successful: item 1") 115 .that(selection.add(SelectionData(1))) 116 .isEqualTo(SelectionModifiedResult.SUCCESS) 117 assertWithMessage("Selection addition was expected to be successful: item 2") 118 .that(selection.toggle(SelectionData(2))) 119 .isEqualTo(SelectionModifiedResult.SUCCESS) 120 assertWithMessage("Selection addition was expected to be successful: item 3") 121 .that(selection.toggleAll(setOf(SelectionData(3)))) 122 .isEqualTo(SelectionModifiedResult.SUCCESS) 123 } 124 125 @Test <lambda>null126 fun testSelectionReturnsSelectionLimitExceededWhenFull() = runTest { 127 val selection: Selection<SelectionData> = 128 GrantsAwareSelectionImpl( 129 scope = backgroundScope, 130 configuration = 131 provideTestConfigurationFlow( 132 scope = backgroundScope, 133 defaultConfiguration = SINGLE_SELECT_CONFIG 134 ), 135 initialSelection = setOf(SelectionData(1)) 136 ) 137 138 139 assertWithMessage("Snapshot was expected to contain the initial selection") 140 .that(selection.add(SelectionData(2))) 141 .isEqualTo(SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED) 142 } 143 144 /** Ensures a single item can be added to the selection. */ 145 @Test <lambda>null146 fun testSelectionCanAddSingleItem() = runTest { 147 val selection: Selection<SelectionData> = 148 GrantsAwareSelectionImpl( 149 scope = backgroundScope, 150 configuration = 151 provideTestConfigurationFlow( 152 scope = backgroundScope, 153 defaultConfiguration = MULTI_SELECT_CONFIG 154 ) 155 ) 156 val emissions = mutableListOf<Set<SelectionData>>() 157 backgroundScope.launch { selection.flow.toList(emissions) } 158 159 val testItem = SelectionData(id = 999) 160 selection.add(testItem) 161 162 val snapshot = selection.snapshot() 163 assertWithMessage("Snapshot does not contain the added item") 164 .that(snapshot) 165 .contains(testItem) 166 assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(1) 167 168 advanceTimeBy(100) 169 170 val flow = emissions.last() 171 assertWithMessage("Emitted flow value does not contain the added item.") 172 .that(flow) 173 .contains(testItem) 174 assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(1) 175 } 176 177 /** Ensures bulk additions. */ 178 @Test <lambda>null179 fun testSelectionCanAddMultipleItems() = runTest { 180 val selection: Selection<SelectionData> = 181 GrantsAwareSelectionImpl( 182 scope = backgroundScope, 183 configuration = 184 provideTestConfigurationFlow( 185 scope = backgroundScope, 186 defaultConfiguration = MULTI_SELECT_CONFIG 187 ) 188 ) 189 val emissions = mutableListOf<Set<SelectionData>>() 190 backgroundScope.launch { selection.flow.toList(emissions) } 191 192 val values = 193 setOf( 194 SelectionData(id = 1), 195 SelectionData(id = 2), 196 SelectionData(id = 3), 197 SelectionData(id = 4), 198 SelectionData(id = 5), 199 SelectionData(id = 6), 200 ) 201 selection.addAll(values) 202 203 advanceTimeBy(100) 204 205 val snapshot = selection.snapshot() 206 assertWithMessage("Snapshot does not contain the added items") 207 .that(snapshot) 208 .containsExactly(*values.toTypedArray()) 209 assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(6) 210 211 assertWithMessage("Emitted flow does not contain the added items") 212 .that(emissions.last()) 213 .containsExactly(*values.toTypedArray()) 214 assertWithMessage("Emitted flow has an unexpected size").that(emissions.last()).hasSize(6) 215 } 216 217 /** Ensures a selection can be reset. */ 218 @Test <lambda>null219 fun testSelectionCanBeCleared() = runTest { 220 val selection: Selection<SelectionData> = 221 GrantsAwareSelectionImpl( 222 scope = backgroundScope, 223 configuration = 224 provideTestConfigurationFlow( 225 scope = backgroundScope, 226 defaultConfiguration = MULTI_SELECT_CONFIG 227 ), 228 initialSelection = INITIAL_SELECTION 229 ) 230 val emissions = mutableListOf<Set<SelectionData>>() 231 backgroundScope.launch { selection.flow.toList(emissions) } 232 233 assertWithMessage("Initial snapshot state does not match expected size") 234 .that(selection.snapshot()) 235 .hasSize(10) 236 237 selection.clear() 238 239 assertWithMessage("Resulting snapshot does not match expected size") 240 .that(selection.snapshot()) 241 .isEmpty() 242 243 advanceTimeBy(100) 244 245 assertWithMessage("Initial flow state does not match expected size") 246 .that(emissions.first()) 247 .hasSize(10) 248 249 assertWithMessage("Resulting flow state does not match expected size") 250 .that(emissions.last()) 251 .isEmpty() 252 } 253 254 /** Ensures a single item can be removed. */ 255 @Test <lambda>null256 fun testSelectionCanRemoveSingleItem() = runTest { 257 val testItem = SelectionData(id = 999) 258 val anotherTestItem = SelectionData(id = 1000) 259 val selection: Selection<SelectionData> = 260 GrantsAwareSelectionImpl( 261 scope = backgroundScope, 262 configuration = 263 provideTestConfigurationFlow( 264 scope = backgroundScope, 265 defaultConfiguration = MULTI_SELECT_CONFIG 266 ), 267 initialSelection = setOf(testItem, anotherTestItem) 268 ) 269 val emissions = mutableListOf<Set<SelectionData>>() 270 backgroundScope.launch { selection.flow.toList(emissions) } 271 272 val initialSnapshot = selection.snapshot() 273 assertWithMessage("Initial Snapshot does not contain the expected item") 274 .that(initialSnapshot) 275 .isEqualTo(setOf(testItem, anotherTestItem)) 276 assertWithMessage("Initial Snapshot has an unexpected size") 277 .that(initialSnapshot) 278 .hasSize(2) 279 280 selection.remove(testItem) 281 282 val snapshot = selection.snapshot() 283 assertWithMessage("Snapshot contains the removed item.") 284 .that(snapshot) 285 .doesNotContain(testItem) 286 assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(1) 287 288 advanceTimeBy(100) 289 290 val flow = emissions.last() 291 assertWithMessage("Emitted flow value contains the removed item.") 292 .that(flow) 293 .doesNotContain(testItem) 294 assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(1) 295 } 296 297 /** Ensures bulk removals. */ 298 @Test <lambda>null299 fun testSelectionCanRemoveMultipleItems() = runTest { 300 val values = 301 setOf( 302 SelectionData(id = 1), 303 SelectionData(id = 2), 304 SelectionData(id = 3), 305 SelectionData(id = 4), 306 SelectionData(id = 5), 307 SelectionData(id = 6), 308 ) 309 310 val selection: Selection<SelectionData> = 311 GrantsAwareSelectionImpl( 312 scope = backgroundScope, 313 configuration = 314 provideTestConfigurationFlow( 315 scope = backgroundScope, 316 defaultConfiguration = MULTI_SELECT_CONFIG 317 ), 318 initialSelection = values 319 ) 320 val emissions = mutableListOf<Set<SelectionData>>() 321 backgroundScope.launch { selection.flow.toList(emissions) } 322 323 val initialSnapshot = selection.snapshot() 324 assertWithMessage("Initial Snapshot has an unexpected size") 325 .that(initialSnapshot) 326 .hasSize(6) 327 328 val removedValues = values.take(3) 329 selection.removeAll(removedValues) 330 331 val snapshot = selection.snapshot() 332 assertWithMessage("Snapshot contains a removed item.") 333 .that(snapshot) 334 .containsNoneIn(removedValues.toTypedArray()) 335 assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(3) 336 337 advanceTimeBy(100) 338 339 val flow = emissions.last() 340 assertWithMessage("Emitted flow value contains the removed item.") 341 .that(flow) 342 .containsNoneIn(removedValues.toTypedArray()) 343 assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(3) 344 } 345 346 /** Ensures a single item can be toggled in and out of the selected set. */ 347 @Test <lambda>null348 fun testSelectionCanToggleSingleItem() = runTest { 349 val selection: Selection<SelectionData> = 350 GrantsAwareSelectionImpl( 351 scope = backgroundScope, 352 configuration = 353 provideTestConfigurationFlow( 354 scope = backgroundScope, 355 defaultConfiguration = MULTI_SELECT_CONFIG 356 ), 357 initialSelection = INITIAL_SELECTION 358 ) 359 val emissions = mutableListOf<Set<SelectionData>>() 360 backgroundScope.launch { selection.flow.toList(emissions) } 361 362 val item = INITIAL_SELECTION.first() 363 364 selection.toggle(item) 365 366 assertWithMessage("Snapshot contained an item that should have been removed") 367 .that(selection.snapshot()) 368 .doesNotContain(item) 369 370 advanceTimeBy(100) 371 assertWithMessage("Flow emission contained an item that should have been removed") 372 .that(emissions.last()) 373 .doesNotContain(item) 374 375 selection.toggle(item) 376 377 assertWithMessage("Snapshot does not contain an item that should have been added") 378 .that(selection.snapshot()) 379 .contains(item) 380 381 advanceTimeBy(100) 382 assertWithMessage("Flow emission does not contain an item that should have been added") 383 .that(emissions.last()) 384 .contains(item) 385 } 386 387 /** Ensures multiple items can be toggled in and out of the selected set. */ 388 @Test <lambda>null389 fun testSelectionCanToggleMultipleItems() = runTest { 390 val selection: Selection<SelectionData> = 391 GrantsAwareSelectionImpl( 392 scope = backgroundScope, 393 configuration = 394 provideTestConfigurationFlow( 395 scope = backgroundScope, 396 defaultConfiguration = MULTI_SELECT_CONFIG 397 ), 398 initialSelection = INITIAL_SELECTION 399 ) 400 val emissions = mutableListOf<Set<SelectionData>>() 401 backgroundScope.launch { selection.flow.toList(emissions) } 402 403 val items = INITIAL_SELECTION.take(3) 404 405 selection.toggleAll(items) 406 407 assertWithMessage("Snapshot contained an item that should have been removed") 408 .that(selection.snapshot()) 409 .containsNoneIn(items) 410 411 advanceTimeBy(100) 412 assertWithMessage("Flow emission contained an item that should have been removed") 413 .that(emissions.last()) 414 .containsNoneIn(items) 415 416 selection.toggleAll(items) 417 418 assertWithMessage("Snapshot does not contain an item that should have been added") 419 .that(selection.snapshot()) 420 .containsAtLeastElementsIn(items) 421 422 advanceTimeBy(100) 423 assertWithMessage("Flow emission does not contain an item that should have been added") 424 .that(emissions.last()) 425 .containsAtLeastElementsIn(items) 426 } 427 428 /** Ensures selection returns the correct position for selected items. */ 429 @Test <lambda>null430 fun testSelectionCanReturnItemPosition() = runTest { 431 val values = 432 listOf( 433 SelectionData(id = 1), 434 SelectionData(id = 2), 435 SelectionData(id = 3), 436 SelectionData(id = 4), 437 SelectionData(id = 5), 438 SelectionData(id = 6), 439 ) 440 441 val selection: Selection<SelectionData> = 442 GrantsAwareSelectionImpl( 443 scope = backgroundScope, 444 configuration = 445 provideTestConfigurationFlow( 446 scope = backgroundScope, 447 defaultConfiguration = MULTI_SELECT_CONFIG 448 ), 449 initialSelection = values 450 ) 451 452 assertWithMessage("Received unexpected position for item.") 453 .that(selection.getPosition(values.get(2))) 454 .isEqualTo(2) 455 } 456 457 /** Ensures selection returns -1 for items not present in the selection. */ 458 @Test <lambda>null459 fun testSelectionGetPositionForMissingItem() = runTest { 460 val selection: Selection<SelectionData> = 461 GrantsAwareSelectionImpl( 462 scope = backgroundScope, 463 configuration = 464 provideTestConfigurationFlow( 465 scope = backgroundScope, 466 defaultConfiguration = MULTI_SELECT_CONFIG 467 ), 468 initialSelection = INITIAL_SELECTION 469 ) 470 471 val missingElement = SelectionData(id = 999) 472 473 assertWithMessage("Received unexpected position for item.") 474 .that(selection.getPosition(missingElement)) 475 .isEqualTo(-1) 476 } 477 478 /** Ensures a single preGranted item can be removed and added again. */ 479 @Test testSelectionCanRemoveSinglePreGrantedItemnull480 fun testSelectionCanRemoveSinglePreGrantedItem() = 481 runTest { 482 // mock a test item to return isPreGranted as true. 483 val testItem = SelectionData(id = 999, isPreGrantedParam = true) 484 485 val selection = 486 GrantsAwareSelectionImpl<SelectionData>( 487 scope = backgroundScope, 488 configuration = 489 provideTestConfigurationFlow( 490 scope = backgroundScope, 491 defaultConfiguration = MULTI_SELECT_CONFIG, 492 ), 493 preGrantedItemsCount = 1, // corresponding to testItem 494 ) 495 496 val emissions = mutableListOf<Set<SelectionData>>() 497 backgroundScope.launch { selection.flow.toList(emissions) } 498 499 val initialSnapshot = selection.snapshot() 500 // There is only one preGranted item 501 assertWithMessage("Initial Snapshot has an unexpected size") 502 .that(initialSnapshot) 503 .hasSize(1) 504 505 // remove preGranted item 506 selection.remove(testItem) 507 508 val snapshot = selection.snapshot() 509 advanceTimeBy(100) 510 val flow = emissions.last() 511 512 assertWithMessage("Deselection should contain test item").that( 513 selection.getDeselection() 514 ).contains(testItem) 515 516 assertWithMessage("Snapshot contains the removed item.") 517 .that(snapshot).doesNotContain(testItem) 518 519 assertWithMessage("Emitted flow value contains the removed item.") 520 .that(flow) 521 .doesNotContain(testItem) 522 assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0) 523 524 // Now add the preGranted item again and verify that it was removed from deselection. 525 selection.add(testItem) 526 527 val snapshot2 = selection.snapshot() 528 advanceTimeBy(100) 529 val flow2 = emissions.last() 530 assertWithMessage("Deselection should not contain test item").that( 531 selection 532 .getDeselection(), 533 ) 534 .doesNotContain(testItem) 535 536 assertWithMessage("Snapshot contains the added item.") 537 .that(snapshot2).contains(testItem) 538 assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1) 539 540 assertWithMessage("Emitted flow value contains the removed item.") 541 .that(flow2) 542 .contains(testItem) 543 assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1) 544 } 545 }