1 /*
<lambda>null2  * Copyright 2019 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 androidx.compose.ui.test.util
18 
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.input.pointer.PointerButtons
21 import androidx.compose.ui.input.pointer.PointerEvent
22 import androidx.compose.ui.input.pointer.PointerEventPass
23 import androidx.compose.ui.input.pointer.PointerEventType
24 import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move
25 import androidx.compose.ui.input.pointer.PointerEventType.Companion.Release
26 import androidx.compose.ui.input.pointer.PointerId
27 import androidx.compose.ui.input.pointer.PointerInputChange
28 import androidx.compose.ui.input.pointer.PointerInputFilter
29 import androidx.compose.ui.input.pointer.PointerInputModifier
30 import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
31 import androidx.compose.ui.input.pointer.PointerType
32 import androidx.compose.ui.input.pointer.util.VelocityTracker
33 import androidx.compose.ui.unit.IntSize
34 import com.google.common.truth.Truth.assertThat
35 import com.google.common.truth.Truth.assertWithMessage
36 
37 data class DataPoint(
38     val id: PointerId,
39     val timestamp: Long,
40     val position: Offset,
41     val scrollDelta: Offset,
42     val down: Boolean,
43     val pointerType: PointerType,
44     val eventType: PointerEventType,
45     val buttons: PointerButtons,
46     val keyboardModifiers: PointerKeyboardModifiers,
47 ) {
48     constructor(
49         change: PointerInputChange,
50         event: PointerEvent
51     ) : this(
52         change.id,
53         change.uptimeMillis,
54         change.position,
55         change.scrollDelta,
56         change.pressed,
57         change.type,
58         event.type,
59         event.buttons,
60         event.keyboardModifiers,
61     )
62 
63     val x
64         get() = position.x
65 
66     val y
67         get() = position.y
68 }
69 
70 /**
71  * A [PointerInputModifier] that records all [PointerEvent]s as they pass through the
72  * [PointerEventPass.Initial] phase, without consuming anything. This modifier is supposed to be
73  * completely transparent to the rest of the system.
74  *
75  * Does not support multiple pointers: all [PointerInputChange]s are flattened in the recorded list.
76  */
77 class SinglePointerInputRecorder : PointerInputModifier {
78     private val _events = mutableListOf<DataPoint>()
79     val events
80         get() = _events as List<DataPoint>
81 
82     private val velocityTracker = VelocityTracker()
83     val recordedVelocity
84         get() = velocityTracker.calculateVelocity()
85 
eventnull86     override val pointerInputFilter = RecordingFilter { event ->
87         event.changes.forEach {
88             _events.add(DataPoint(it, event))
89             velocityTracker.addPosition(it.uptimeMillis, it.position)
90         }
91     }
92 }
93 
94 /**
95  * A [PointerInputModifier] that records all [PointerEvent]s as they pass through the
96  * [PointerEventPass.Initial] phase, without consuming anything. This modifier is supposed to be
97  * completely transparent to the rest of the system.
98  *
99  * Supports multiple pointers: the set of [PointerInputChange]s from each event is kept together in
100  * the recorded list.
101  */
102 class MultiPointerInputRecorder : PointerInputModifier {
103     data class Event(val pointers: List<DataPoint>) {
104         val pointerCount: Int
105             get() = pointers.size
106 
getPointernull107         fun getPointer(index: Int) = pointers[index]
108     }
109 
110     private val _events = mutableListOf<Event>()
111     val events
112         get() = _events as List<Event>
113 
114     override val pointerInputFilter = RecordingFilter { event ->
115         _events.add(Event(event.changes.map { DataPoint(it, event) }))
116     }
117 }
118 
119 /**
120  * A [PointerInputFilter] that [record]s each [PointerEvent][onPointerEvent] during the
121  * [PointerEventPass.Initial] pass. Does not consume anything itself, although implementation can
122  * (but really shouldn't).
123  */
124 class RecordingFilter(private val record: (PointerEvent) -> Unit) : PointerInputFilter() {
onPointerEventnull125     override fun onPointerEvent(
126         pointerEvent: PointerEvent,
127         pass: PointerEventPass,
128         bounds: IntSize
129     ) {
130         if (pass == PointerEventPass.Initial) {
131             record(pointerEvent)
132         }
133     }
134 
onCancelnull135     override fun onCancel() {
136         // Do nothing
137     }
138 }
139 
140 val SinglePointerInputRecorder.downEvents
<lambda>null141     get() = events.filter { it.down }
142 
143 val SinglePointerInputRecorder.recordedDurationMillis: Long
144     get() {
<lambda>null145         check(events.isNotEmpty()) { "No events recorded" }
146         return events.last().timestamp - events.first().timestamp
147     }
148 
SinglePointerInputRecordernull149 fun SinglePointerInputRecorder.assertTimestampsAreIncreasing() {
150     check(events.isNotEmpty()) { "No events recorded" }
151     events.reduce { prev, curr ->
152         assertThat(curr.timestamp).isAtLeast(prev.timestamp)
153         curr
154     }
155 }
156 
assertTimestampsAreIncreasingnull157 fun MultiPointerInputRecorder.assertTimestampsAreIncreasing() {
158     check(events.isNotEmpty()) { "No events recorded" }
159     // Check that each event has the same timestamp
160     events.forEach { event ->
161         assertThat(event.pointerCount).isAtLeast(1)
162         val currTime = event.pointers[0].timestamp
163         for (i in 1 until event.pointerCount) {
164             assertThat(event.pointers[i].timestamp).isEqualTo(currTime)
165         }
166     }
167     // Check that the timestamps are ordered
168     assertThat(events.map { it.pointers[0].timestamp }).isInOrder()
169 }
170 
SinglePointerInputRecordernull171 fun SinglePointerInputRecorder.assertOnlyLastEventIsUp() {
172     check(events.isNotEmpty()) { "No events recorded" }
173     assertThat(events.last().down).isFalse()
174     assertThat(events.count { !it.down }).isEqualTo(1)
175 }
176 
SinglePointerInputRecordernull177 fun SinglePointerInputRecorder.assertUpSameAsLastMove() {
178     check(events.isNotEmpty()) { "No events recorded" }
179     events.last().let {
180         assertThat(it.eventType).isEqualTo(Release)
181         downEvents.last().verify(it.timestamp, it.id, true, it.position, it.pointerType, Move)
182     }
183 }
184 
assertSinglePointernull185 fun SinglePointerInputRecorder.assertSinglePointer() {
186     assertThat(events.map { it.id }.distinct()).hasSize(1)
187 }
188 
SinglePointerInputRecordernull189 fun SinglePointerInputRecorder.verifyEvents(vararg verifiers: DataPoint.() -> Unit) {
190     assertThat(events).hasSize(verifiers.size)
191     if (events.isNotEmpty()) {
192         assertTimestampsAreIncreasing()
193         events.zip(verifiers) { event, verification -> verification.invoke(event) }
194     }
195 }
196 
DataPointnull197 fun DataPoint.verify(
198     expectedTimestamp: Long?,
199     expectedId: PointerId?,
200     expectedDown: Boolean,
201     expectedPosition: Offset,
202     expectedPointerType: PointerType,
203     expectedEventType: PointerEventType,
204     expectedScrollDelta: Offset = Offset.Zero,
205     expectedButtons: PointerButtons = PointerButtons(0),
206     expectedKeyboardModifiers: PointerKeyboardModifiers = PointerKeyboardModifiers(0),
207 ) {
208     val s = " of $this"
209     if (expectedTimestamp != null) {
210         assertWithMessage("timestamp$s").that(timestamp).isEqualTo(expectedTimestamp)
211     }
212     if (expectedId != null) {
213         assertWithMessage("pointerId$s").that(id).isEqualTo(expectedId)
214     }
215     assertWithMessage("isDown$s").that(down).isEqualTo(expectedDown)
216     position.isAlmostEqualTo(expectedPosition, message = "position$s")
217     assertWithMessage("pointerType$s").that(pointerType).isEqualTo(expectedPointerType)
218     assertWithMessage("eventType$s").that(eventType).isEqualTo(expectedEventType)
219     scrollDelta.isAlmostEqualTo(expectedScrollDelta, message = "scrollDelta$s")
220     assertWithMessage("buttonsDown$s").that(buttons).isEqualTo(expectedButtons)
221     assertWithMessage("keyModifiers$s").that(keyboardModifiers).isEqualTo(expectedKeyboardModifiers)
222 }
223 
224 /** Checks that the coordinates are progressing in a monotonous direction */
isMonotonicBetweennull225 fun List<DataPoint>.isMonotonicBetween(start: Offset, end: Offset) {
226     map { it.x }.isMonotonicBetween(start.x, end.x, 1e-3f)
227     map { it.y }.isMonotonicBetween(start.y, end.y, 1e-3f)
228 }
229 
hasSameTimeBetweenEventsnull230 fun List<DataPoint>.hasSameTimeBetweenEvents() {
231     zipWithNext { a, b -> b.timestamp - a.timestamp }
232         .sorted()
233         .apply { assertThat(last() - first()).isAtMost(1L) }
234 }
235 
areSampledFromCurvenull236 fun List<DataPoint>.areSampledFromCurve(curve: (Long) -> Offset) {
237     val t0 = first().timestamp
238     forEach { it.position.isAlmostEqualTo(curve(it.timestamp - t0)) }
239 }
240