1 /* 2 * 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.systemui.haptics.slider 18 19 import androidx.test.ext.junit.runners.AndroidJUnit4 20 import androidx.test.filters.SmallTest 21 import com.android.systemui.SysuiTestCase 22 import com.google.common.truth.Truth.assertThat 23 import kotlinx.coroutines.CoroutineScope 24 import kotlinx.coroutines.test.UnconfinedTestDispatcher 25 import kotlinx.coroutines.test.advanceTimeBy 26 import kotlinx.coroutines.test.runTest 27 import org.junit.Before 28 import org.junit.Test 29 import org.junit.runner.RunWith 30 import org.mockito.Mock 31 import org.mockito.Mockito.anyFloat 32 import org.mockito.Mockito.verify 33 import org.mockito.Mockito.verifyNoMoreInteractions 34 import org.mockito.MockitoAnnotations 35 36 @SmallTest 37 @RunWith(AndroidJUnit4::class) 38 class SliderStateTrackerTest : SysuiTestCase() { 39 40 @Mock private lateinit var sliderStateListener: SliderStateListener 41 private val sliderEventProducer = FakeSliderEventProducer() 42 private lateinit var mSliderStateTracker: SliderStateTracker 43 44 @Before setupnull45 fun setup() { 46 MockitoAnnotations.initMocks(this) 47 } 48 49 @Test initializeSliderTracker_startsTrackingnull50 fun initializeSliderTracker_startsTracking() = runTest { 51 // GIVEN Initialized tracker 52 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 53 54 // THEN the tracker job is active 55 assertThat(mSliderStateTracker.isTracking).isTrue() 56 } 57 58 @Test <lambda>null59 fun stopTracking_onAnyState_resetsToIdle() = runTest { 60 enumValues<SliderState>().forEach { 61 // GIVEN Initialized tracker 62 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 63 64 // GIVEN a state in the state machine 65 mSliderStateTracker.setState(it) 66 67 // WHEN the tracker stops tracking the state and listening to events 68 mSliderStateTracker.stopTracking() 69 70 // THEN The state is idle and the tracker is not active 71 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 72 assertThat(mSliderStateTracker.isTracking).isFalse() 73 } 74 } 75 76 // Tests on the IDLE state 77 @Test <lambda>null78 fun initializeSliderTracker_isIdle() = runTest { 79 // GIVEN Initialized tracker 80 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 81 82 // THEN The state is idle and the listener is not called to play haptics 83 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 84 verifyNoMoreInteractions(sliderStateListener) 85 } 86 87 @Test <lambda>null88 fun startsTrackingTouch_onIdle_entersWaitState() = runTest { 89 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 90 91 // GIVEN a start of tracking touch event 92 val progress = 0f 93 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 94 95 // THEN the tracker moves to the wait state and the timer job begins 96 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) 97 verifyNoMoreInteractions(sliderStateListener) 98 assertThat(mSliderStateTracker.isWaiting).isTrue() 99 } 100 101 // Tests on the WAIT state 102 103 @Test <lambda>null104 fun waitCompletes_onWait_movesToHandleAcquired() = runTest { 105 val config = SeekableSliderTrackerConfig() 106 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 107 108 // GIVEN a start of tracking touch event that moves the tracker to WAIT 109 val progress = 0f 110 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 111 112 // WHEN the wait time completes plus a small buffer time 113 advanceTimeBy(config.waitTimeMillis + 10L) 114 115 // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state 116 assertThat(mSliderStateTracker.currentState) 117 .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) 118 assertThat(mSliderStateTracker.isWaiting).isFalse() 119 verify(sliderStateListener).onHandleAcquiredByTouch() 120 verifyNoMoreInteractions(sliderStateListener) 121 } 122 123 @Test <lambda>null124 fun impreciseTouch_onWait_movesToHandleAcquired() = runTest { 125 val config = SeekableSliderTrackerConfig() 126 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 127 128 // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the 129 // slider 130 var progress = 0.5f 131 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 132 133 // GIVEN a progress event due to an imprecise touch with a progress below threshold 134 progress += (config.jumpThreshold - 0.01f) 135 sliderEventProducer.sendEvent( 136 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 137 ) 138 139 // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state without the timer job 140 // being complete 141 assertThat(mSliderStateTracker.currentState) 142 .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) 143 assertThat(mSliderStateTracker.isWaiting).isFalse() 144 verify(sliderStateListener).onHandleAcquiredByTouch() 145 verifyNoMoreInteractions(sliderStateListener) 146 } 147 148 @Test <lambda>null149 fun trackJump_onWait_movesToJumpTrackLocationSelected() = runTest { 150 val config = SeekableSliderTrackerConfig() 151 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 152 153 // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the 154 // slider 155 var progress = 0.5f 156 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 157 158 // GIVEN a progress event due to a touch on the slider track beyond threshold 159 progress += (config.jumpThreshold + 0.01f) 160 sliderEventProducer.sendEvent( 161 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 162 ) 163 164 // THEN the tracker moves to the jump-track location selected state 165 assertThat(mSliderStateTracker.currentState) 166 .isEqualTo(SliderState.JUMP_TRACK_LOCATION_SELECTED) 167 assertThat(mSliderStateTracker.isWaiting).isFalse() 168 verify(sliderStateListener).onProgressJump(anyFloat()) 169 verifyNoMoreInteractions(sliderStateListener) 170 } 171 172 @Test <lambda>null173 fun upperBookendSelection_onWait_movesToBookendSelected() = runTest { 174 val config = SeekableSliderTrackerConfig() 175 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 176 177 // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the 178 // slider 179 var progress = 0.5f 180 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 181 182 // GIVEN a progress event due to a touch on the slider upper bookend zone. 183 progress = (config.upperBookendThreshold + 0.01f) 184 sliderEventProducer.sendEvent( 185 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 186 ) 187 188 // THEN the tracker moves to the jump-track location selected state 189 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) 190 assertThat(mSliderStateTracker.isWaiting).isFalse() 191 verifyNoMoreInteractions(sliderStateListener) 192 } 193 194 @Test <lambda>null195 fun lowerBookendSelection_onWait_movesToBookendSelected() = runTest { 196 val config = SeekableSliderTrackerConfig() 197 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 198 199 // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the 200 // slider 201 var progress = 0.5f 202 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 203 204 // GIVEN a progress event due to a touch on the slider lower bookend zone 205 progress = (config.lowerBookendThreshold - 0.01f) 206 sliderEventProducer.sendEvent( 207 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 208 ) 209 210 // THEN the tracker moves to the JUMP_TRACK_LOCATION_SELECTED state 211 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) 212 assertThat(mSliderStateTracker.isWaiting).isFalse() 213 verifyNoMoreInteractions(sliderStateListener) 214 } 215 216 @Test <lambda>null217 fun stopTracking_onWait_whenWaitingJobIsActive_resetsToIdle() = runTest { 218 val config = SeekableSliderTrackerConfig() 219 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 220 221 // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the 222 // slider 223 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5f)) 224 assertThat(mSliderStateTracker.isWaiting).isTrue() 225 226 // GIVEN that the tracker stops tracking the state and listening to events 227 mSliderStateTracker.stopTracking() 228 229 // THEN the tracker moves to the IDLE state without the timer job being complete 230 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 231 assertThat(mSliderStateTracker.isWaiting).isFalse() 232 assertThat(mSliderStateTracker.isTracking).isFalse() 233 verifyNoMoreInteractions(sliderStateListener) 234 } 235 236 // Tests on the JUMP_TRACK_LOCATION_SELECTED state 237 238 @Test <lambda>null239 fun progressChangeByUser_onJumpTrackLocationSelected_movesToDragHandleDragging() = runTest { 240 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 241 242 // GIVEN a JUMP_TRACK_LOCATION_SELECTED state 243 mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) 244 245 // GIVEN a progress event due to dragging the handle 246 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f)) 247 248 // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state 249 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 250 verify(sliderStateListener).onProgress(anyFloat()) 251 verifyNoMoreInteractions(sliderStateListener) 252 } 253 254 @Test <lambda>null255 fun touchRelease_onJumpTrackLocationSelected_movesToIdle() = runTest { 256 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 257 258 // GIVEN a JUMP_TRACK_LOCATION_SELECTED state 259 mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) 260 261 // GIVEN that the slider stopped tracking touch 262 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) 263 264 // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state 265 verify(sliderStateListener).onHandleReleasedFromTouch() 266 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 267 verifyNoMoreInteractions(sliderStateListener) 268 } 269 270 @Test <lambda>null271 fun progressChangeByUser_onJumpBookendSelected_movesToDragHandleDragging() = runTest { 272 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 273 274 // GIVEN a JUMP_BOOKEND_SELECTED state 275 mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) 276 277 // GIVEN that the slider stopped tracking touch 278 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f)) 279 280 // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state 281 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 282 verify(sliderStateListener).onProgress(anyFloat()) 283 verifyNoMoreInteractions(sliderStateListener) 284 } 285 286 @Test <lambda>null287 fun touchRelease_onJumpBookendSelected_movesToIdle() = runTest { 288 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 289 290 // GIVEN a JUMP_BOOKEND_SELECTED state 291 mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) 292 293 // GIVEN that the slider stopped tracking touch 294 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) 295 296 // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state 297 verify(sliderStateListener).onHandleReleasedFromTouch() 298 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 299 verifyNoMoreInteractions(sliderStateListener) 300 } 301 302 // Tests on the DRAG_HANDLE_ACQUIRED state 303 304 @Test <lambda>null305 fun progressChangeByUser_onHandleAcquired_movesToDragHandleDragging() = runTest { 306 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 307 308 // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state 309 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) 310 311 // GIVEN a progress change by the user 312 val progress = 0.5f 313 sliderEventProducer.sendEvent( 314 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 315 ) 316 317 // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state 318 verify(sliderStateListener).onProgress(progress) 319 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 320 verifyNoMoreInteractions(sliderStateListener) 321 } 322 323 @Test touchRelease_onHandleAcquired_movesToIdlenull324 fun touchRelease_onHandleAcquired_movesToIdle() = runTest { 325 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 326 327 // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state 328 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) 329 330 // GIVEN that the handle stops tracking touch 331 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) 332 333 // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state 334 verify(sliderStateListener).onHandleReleasedFromTouch() 335 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 336 verifyNoMoreInteractions(sliderStateListener) 337 } 338 339 // Tests on DRAG_HANDLE_DRAGGING 340 341 @Test progressChangeByUser_onHandleDragging_progressOutsideOfBookends_doesNotChangeStatenull342 fun progressChangeByUser_onHandleDragging_progressOutsideOfBookends_doesNotChangeState() = 343 runTest { 344 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 345 346 // GIVEN a DRAG_HANDLE_DRAGGING state 347 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) 348 349 // GIVEN a progress change by the user outside of bookend bounds 350 val progress = 0.5f 351 sliderEventProducer.sendEvent( 352 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 353 ) 354 355 // THEN the tracker does not change state and executes the onProgress call 356 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 357 verify(sliderStateListener).onProgress(progress) 358 verifyNoMoreInteractions(sliderStateListener) 359 } 360 361 @Test progressChangeByUser_onHandleDragging_reachesLowerBookend_movesToHandleReachedBookendnull362 fun progressChangeByUser_onHandleDragging_reachesLowerBookend_movesToHandleReachedBookend() = 363 runTest { 364 val config = SeekableSliderTrackerConfig() 365 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 366 367 // GIVEN a DRAG_HANDLE_DRAGGING state 368 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) 369 370 // GIVEN a progress change by the user reaching the lower bookend 371 val progress = config.lowerBookendThreshold - 0.01f 372 sliderEventProducer.sendEvent( 373 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 374 ) 375 376 // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the 377 // corresponding callback 378 assertThat(mSliderStateTracker.currentState) 379 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 380 verify(sliderStateListener).onLowerBookend() 381 verifyNoMoreInteractions(sliderStateListener) 382 } 383 384 @Test progressChangeByUser_onHandleDragging_reachesUpperBookend_movesToHandleReachedBookendnull385 fun progressChangeByUser_onHandleDragging_reachesUpperBookend_movesToHandleReachedBookend() = 386 runTest { 387 val config = SeekableSliderTrackerConfig() 388 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 389 390 // GIVEN a DRAG_HANDLE_DRAGGING state 391 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) 392 393 // GIVEN a progress change by the user reaching the upper bookend 394 val progress = config.upperBookendThreshold + 0.01f 395 sliderEventProducer.sendEvent( 396 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 397 ) 398 399 // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the 400 // corresponding callback 401 assertThat(mSliderStateTracker.currentState) 402 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 403 verify(sliderStateListener).onUpperBookend() 404 verifyNoMoreInteractions(sliderStateListener) 405 } 406 407 @Test <lambda>null408 fun touchRelease_onHandleDragging_movesToIdle() = runTest { 409 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 410 411 // GIVEN a DRAG_HANDLE_DRAGGING state 412 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) 413 414 // GIVEN that the slider stops tracking touch 415 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) 416 417 // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state 418 verify(sliderStateListener).onHandleReleasedFromTouch() 419 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 420 verifyNoMoreInteractions(sliderStateListener) 421 } 422 423 // Tests on the DRAG_HANDLE_REACHED_BOOKEND state 424 425 @Test progressChangeByUser_outsideOfBookendRange_onLowerBookend_movesToDragHandleDraggingnull426 fun progressChangeByUser_outsideOfBookendRange_onLowerBookend_movesToDragHandleDragging() = 427 runTest { 428 val config = SeekableSliderTrackerConfig() 429 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 430 431 // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state 432 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 433 434 // GIVEN a progress event that falls outside of the lower bookend range 435 val progress = config.lowerBookendThreshold + 0.01f 436 sliderEventProducer.sendEvent( 437 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 438 ) 439 440 // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly 441 verify(sliderStateListener).onProgress(progress) 442 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 443 verifyNoMoreInteractions(sliderStateListener) 444 } 445 446 @Test <lambda>null447 fun progressChangeByUser_insideOfBookendRange_onLowerBookend_doesNotChangeState() = runTest { 448 val config = SeekableSliderTrackerConfig() 449 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 450 451 // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state 452 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 453 454 // GIVEN a progress event that falls inside of the lower bookend range 455 val progress = config.lowerBookendThreshold - 0.01f 456 sliderEventProducer.sendEvent( 457 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 458 ) 459 460 // THEN the tracker stays in the current state and executes accordingly 461 verify(sliderStateListener).onLowerBookend() 462 assertThat(mSliderStateTracker.currentState) 463 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 464 verifyNoMoreInteractions(sliderStateListener) 465 } 466 467 @Test progressChangeByUser_outsideOfBookendRange_onUpperBookend_movesToDragHandleDraggingnull468 fun progressChangeByUser_outsideOfBookendRange_onUpperBookend_movesToDragHandleDragging() = 469 runTest { 470 val config = SeekableSliderTrackerConfig() 471 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 472 473 // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state 474 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 475 476 // GIVEN a progress event that falls outside of the upper bookend range 477 val progress = config.upperBookendThreshold - 0.01f 478 sliderEventProducer.sendEvent( 479 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 480 ) 481 482 // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly 483 verify(sliderStateListener).onProgress(progress) 484 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) 485 verifyNoMoreInteractions(sliderStateListener) 486 } 487 488 @Test <lambda>null489 fun progressChangeByUser_insideOfBookendRange_onUpperBookend_doesNotChangeState() = runTest { 490 val config = SeekableSliderTrackerConfig() 491 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 492 493 // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state 494 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 495 496 // GIVEN a progress event that falls inside of the upper bookend range 497 val progress = config.upperBookendThreshold + 0.01f 498 sliderEventProducer.sendEvent( 499 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress) 500 ) 501 502 // THEN the tracker stays in the current state and executes accordingly 503 verify(sliderStateListener).onUpperBookend() 504 assertThat(mSliderStateTracker.currentState) 505 .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 506 verifyNoMoreInteractions(sliderStateListener) 507 } 508 509 @Test <lambda>null510 fun touchRelease_onHandleReachedBookend_movesToIdle() = runTest { 511 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 512 513 // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state 514 mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) 515 516 // GIVEN that the handle stops tracking touch 517 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) 518 519 // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state 520 verify(sliderStateListener).onHandleReleasedFromTouch() 521 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 522 verifyNoMoreInteractions(sliderStateListener) 523 } 524 525 @Test <lambda>null526 fun onStartedTrackingProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest { 527 // GIVEN an initialized tracker in the IDLE state 528 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 529 530 // GIVEN a progress due to an external source that lands at the middle of the slider 531 val progress = 0.5f 532 sliderEventProducer.sendEvent( 533 SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) 534 ) 535 536 // THEN the state moves to ARROW_HANDLE_MOVED_ONCE and the listener is called to play 537 // haptics 538 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) 539 verify(sliderStateListener).onSelectAndArrow(progress) 540 } 541 542 @Test <lambda>null543 fun onStartedTrackingProgram_atUpperBookend_onIdle_movesToIdle() = runTest { 544 // GIVEN an initialized tracker in the IDLE state 545 val config = SeekableSliderTrackerConfig() 546 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 547 548 // GIVEN a progress due to an external source that lands at the upper bookend 549 val progress = config.upperBookendThreshold + 0.01f 550 sliderEventProducer.sendEvent( 551 SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) 552 ) 553 554 // THEN the tracker executes upper bookend haptics before moving back to IDLE 555 verify(sliderStateListener).onUpperBookend() 556 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 557 } 558 559 @Test onStartedTrackingProgram_atLowerBookend_onIdle_movesToIdlenull560 fun onStartedTrackingProgram_atLowerBookend_onIdle_movesToIdle() = runTest { 561 // GIVEN an initialized tracker in the IDLE state 562 val config = SeekableSliderTrackerConfig() 563 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 564 565 // WHEN a progress is recorded due to an external source that lands at the lower bookend 566 val progress = config.lowerBookendThreshold - 0.01f 567 sliderEventProducer.sendEvent( 568 SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) 569 ) 570 571 // THEN the tracker executes lower bookend haptics before moving to IDLE 572 verify(sliderStateListener).onLowerBookend() 573 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 574 } 575 576 @Test <lambda>null577 fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest { 578 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state 579 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 580 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) 581 582 // WHEN the external stimulus is released 583 val progress = 0.5f 584 sliderEventProducer.sendEvent( 585 SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress) 586 ) 587 588 // THEN the tracker moves back to IDLE and there are no haptics 589 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 590 verifyNoMoreInteractions(sliderStateListener) 591 } 592 593 @Test <lambda>null594 fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest { 595 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state 596 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 597 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) 598 599 // WHEN the slider starts tracking touch 600 val progress = 0.5f 601 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 602 603 // THEN the tracker moves back to WAIT and starts the waiting job. Also, there are no 604 // haptics 605 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) 606 assertThat(mSliderStateTracker.isWaiting).isTrue() 607 verifyNoMoreInteractions(sliderStateListener) 608 } 609 610 @Test <lambda>null611 fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest { 612 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state 613 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 614 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) 615 616 // WHEN the slider gets an external progress change 617 val progress = 0.5f 618 sliderEventProducer.sendEvent( 619 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) 620 ) 621 622 // THEN the tracker moves to ARROW_HANDLE_MOVES_CONTINUOUSLY and calls the appropriate 623 // haptics 624 assertThat(mSliderStateTracker.currentState) 625 .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 626 verify(sliderStateListener).onProgress(progress) 627 } 628 629 @Test <lambda>null630 fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest { 631 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state 632 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 633 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 634 635 // WHEN the external stimulus is released 636 val progress = 0.5f 637 sliderEventProducer.sendEvent( 638 SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress) 639 ) 640 641 // THEN the tracker moves to IDLE and no haptics are played 642 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 643 verifyNoMoreInteractions(sliderStateListener) 644 } 645 646 @Test <lambda>null647 fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest { 648 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state 649 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 650 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 651 652 // WHEN the slider starts tracking touch 653 val progress = 0.5f 654 sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) 655 656 // THEN the tracker moves to WAIT and the wait job starts. 657 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) 658 assertThat(mSliderStateTracker.isWaiting).isTrue() 659 verifyNoMoreInteractions(sliderStateListener) 660 } 661 662 @Test <lambda>null663 fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest { 664 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state 665 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) 666 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 667 668 // WHEN the slider changes progress programmatically at the middle 669 val progress = 0.5f 670 sliderEventProducer.sendEvent( 671 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) 672 ) 673 674 // THEN the tracker stays in the same state and haptics are delivered appropriately 675 assertThat(mSliderStateTracker.currentState) 676 .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 677 verify(sliderStateListener).onProgress(progress) 678 } 679 680 @Test <lambda>null681 fun onProgramProgress_atLowerBookend_onArrowMovesContinuously_movesToIdle() = runTest { 682 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state 683 val config = SeekableSliderTrackerConfig() 684 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 685 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 686 687 // WHEN the slider reaches the lower bookend programmatically 688 val progress = config.lowerBookendThreshold - 0.01f 689 sliderEventProducer.sendEvent( 690 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) 691 ) 692 693 // THEN the tracker executes lower bookend haptics before moving to IDLE 694 verify(sliderStateListener).onLowerBookend() 695 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 696 } 697 698 @Test <lambda>null699 fun onProgramProgress_atUpperBookend_onArrowMovesContinuously_movesToIdle() = runTest { 700 // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state 701 val config = SeekableSliderTrackerConfig() 702 initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) 703 mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) 704 705 // WHEN the slider reaches the lower bookend programmatically 706 val progress = config.upperBookendThreshold + 0.01f 707 sliderEventProducer.sendEvent( 708 SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) 709 ) 710 711 // THEN the tracker executes upper bookend haptics before moving to IDLE 712 verify(sliderStateListener).onUpperBookend() 713 assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) 714 } 715 initTrackernull716 private fun initTracker( 717 scope: CoroutineScope, 718 config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), 719 ) { 720 mSliderStateTracker = 721 SliderStateTracker(sliderStateListener, sliderEventProducer, scope, config) 722 mSliderStateTracker.startTracking() 723 } 724 } 725