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