• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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