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 android.view.InputDevice
20 import android.view.InputEvent
21 import android.view.KeyEvent
22 import android.view.MotionEvent
23 import androidx.compose.ui.geometry.Offset
24 import androidx.compose.ui.test.InputDispatcher
25 import androidx.compose.ui.test.MultiModalInjectionScopeImpl
26 import androidx.compose.ui.test.SemanticsNodeInteraction
27 import com.google.common.collect.Ordering
28 import com.google.common.truth.FloatSubject
29 import com.google.common.truth.Truth.assertThat
30 import com.google.common.truth.Truth.assertWithMessage
31 import kotlin.math.abs
32 import kotlin.math.sign
33
34 const val TypeFinger = MotionEvent.TOOL_TYPE_FINGER
35 const val TypeMouse = MotionEvent.TOOL_TYPE_MOUSE
36 const val SourceTouchscreen = InputDevice.SOURCE_TOUCHSCREEN
37 const val SourceMouse = InputDevice.SOURCE_MOUSE
38
39 internal fun SemanticsNodeInteraction.assertNoTouchGestureInProgress() {
40 val failMessage = "Can't verify if a touch is in progress: failed to create an injection scope"
41 val node = fetchSemanticsNode(failMessage)
42 val scope = MultiModalInjectionScopeImpl(node, testContext)
43 assertThat(scope.inputDispatcher.isTouchInProgress).isFalse()
44 }
45
assertNoTouchGestureInProgressnull46 internal fun InputDispatcher.assertNoTouchGestureInProgress() {
47 assertThat(isTouchInProgress).isFalse()
48 }
49
50 /**
51 * Asserts that all event times are after their corresponding down time, and that the event stream
52 * has increasing event times.
53 */
assertHasValidEventTimesnull54 internal fun InputEventRecorder.assertHasValidEventTimes() {
55 events.fold(Pair(0L, 0L)) { (lastDownTime, lastEventTime), event ->
56 var downTime: Long = 0
57
58 when (event) {
59 is MotionEvent -> downTime = event.downTime
60 is KeyEvent -> downTime = event.downTime
61 else ->
62 AssertionError(
63 "Given InputEvent must be a MotionEvent or KeyEvent" +
64 " not ${event::class.simpleName}"
65 )
66 }
67
68 assertWithMessage("monotonically increasing downTime")
69 .that(downTime)
70 .isAtLeast(lastDownTime)
71 assertWithMessage("monotonically increasing eventTime")
72 .that(event.eventTime)
73 .isAtLeast(lastEventTime)
74 assertWithMessage("downTime <= eventTime").that(downTime).isAtMost(event.eventTime)
75 Pair(downTime, event.eventTime)
76 }
77 }
78
verifyTouchEventnull79 internal fun InputEvent.verifyTouchEvent(
80 expectedPointerCount: Int,
81 expectedAction: Int,
82 expectedActionIndex: Int,
83 expectedRelativeTime: Long
84 ) {
85 if (this is MotionEvent) {
86 assertThat(pointerCount).isEqualTo(expectedPointerCount)
87 assertThat(actionMasked).isEqualTo(expectedAction)
88 assertThat(actionIndex).isEqualTo(expectedActionIndex)
89 assertThat(relativeTime).isEqualTo(expectedRelativeTime)
90 assertThat(source).isEqualTo(SourceTouchscreen)
91 } else {
92 throw AssertionError(
93 "A touch event must be of type MotionEvent, " + "not ${this::class.simpleName}"
94 )
95 }
96 }
97
verifyTouchPointernull98 internal fun InputEvent.verifyTouchPointer(expectedPointerId: Int, expectedPosition: Offset) {
99 if (this is MotionEvent) {
100 var index = -1
101 for (i in 0 until pointerCount) {
102 if (getPointerId(i) == expectedPointerId) {
103 index = i
104 break
105 }
106 }
107 assertThat(index).isAtLeast(0)
108 assertThat(getX(index)).isEqualTo(expectedPosition.x)
109 assertThat(getY(index)).isEqualTo(expectedPosition.y)
110 assertThat(getToolType(index)).isEqualTo(TypeFinger)
111 } else {
112 throw AssertionError(
113 "A touch event must be of type MotionEvent, " + "not ${this::class.simpleName}"
114 )
115 }
116 }
117
verifyMouseEventnull118 internal fun InputEvent.verifyMouseEvent(
119 expectedAction: Int,
120 expectedRelativeTime: Long,
121 expectedPosition: Offset,
122 expectedButtonState: Int,
123 vararg expectedAxisValues: Pair<Int, Float>, // <axis, value>
124 expectedMetaState: Int = 0,
125 ) {
126 if (this is MotionEvent) {
127 assertWithMessage("pointerCount").that(pointerCount).isEqualTo(1)
128 assertWithMessage("pointerId").that(getPointerId(0)).isEqualTo(0)
129 assertWithMessage("actionMasked").that(actionMasked).isEqualTo(expectedAction)
130 assertWithMessage("actionIndex").that(actionIndex).isEqualTo(0)
131 assertWithMessage("relativeTime").that(relativeTime).isEqualTo(expectedRelativeTime)
132 assertWithMessage("x").that(x).isEqualTo(expectedPosition.x)
133 assertWithMessage("y").that(y).isEqualTo(expectedPosition.y)
134 assertWithMessage("buttonState").that(buttonState).isEqualTo(expectedButtonState)
135 assertWithMessage("source").that(source).isEqualTo(SourceMouse)
136 assertWithMessage("toolType").that(getToolType(0)).isEqualTo(TypeMouse)
137 assertWithMessage("metaState").that(metaState).isEqualTo(expectedMetaState)
138 expectedAxisValues.forEach { (axis, expectedValue) ->
139 assertWithMessage("axisValue($axis)").that(getAxisValue(axis)).isEqualTo(expectedValue)
140 }
141 } else {
142 throw AssertionError(
143 "A mouse event must be of type MotionEvent, " + "not ${this::class.simpleName}"
144 )
145 }
146 }
147
verifyKeyEventnull148 internal fun InputEvent.verifyKeyEvent(
149 expectedAction: Int,
150 expectedKeyCode: Int,
151 expectedEventTime: Long = 0,
152 expectedDownTime: Long = 0,
153 expectedMetaState: Int = 0,
154 expectedRepeat: Int = 0,
155 ) {
156 if (this is KeyEvent) {
157 assertWithMessage("action").that(action).isEqualTo(expectedAction)
158 assertWithMessage("keyCode").that(keyCode).isEqualTo(expectedKeyCode)
159 assertWithMessage("eventTime").that(eventTime).isEqualTo(expectedEventTime)
160 assertWithMessage("downTime").that(downTime).isEqualTo(expectedDownTime)
161 assertWithMessage("metaState").that(metaState).isEqualTo(expectedMetaState)
162 assertWithMessage("repeat").that(repeatCount).isEqualTo(expectedRepeat)
163 } else {
164 throw AssertionError(
165 "A keyboard event must be of type KeyEvent, " + "not ${this::class.simpleName}"
166 )
167 }
168 }
169
170 /** Returns a list of all events between [t0] and [t1], excluding [t0] and including [t1]. */
Listnull171 fun List<MotionEvent>.between(t0: Long, t1: Long): List<MotionEvent> {
172 return dropWhile { it.relativeTime <= t0 }.takeWhile { it.relativeTime <= t1 }
173 }
174
175 /** Checks that the coordinates are progressing in a monotonous direction */
isMonotonicBetweennull176 fun List<MotionEvent>.isMonotonicBetween(start: Offset, end: Offset) {
177 map { it.x }.isMonotonicBetween(start.x, end.x, 1e-6f)
178 map { it.y }.isMonotonicBetween(start.y, end.y, 1e-6f)
179 }
180
181 /**
182 * Verifies that the MotionEvents in this list are equidistant from each other in time between [t0]
183 * and [t1], with a duration between them that is as close to the [desiredDuration] as possible,
184 * given that the sequence is splitting the total duration between [t0] and [t1].
185 */
Listnull186 fun List<MotionEvent>.splitsDurationEquallyInto(t0: Long, t1: Long, desiredDuration: Long) {
187 val totalDuration = t1 - t0
188 if (totalDuration < desiredDuration) {
189 assertThat(this).hasSize(1)
190 assertThat(first().relativeTime - t0).isEqualTo(totalDuration)
191 return
192 }
193
194 // Either `desiredDuration` divides `totalDuration` perfectly, or it doesn't.
195 // If it doesn't, `desiredDuration * size` must be as close to `totalDuration` as possible.
196 // Verify that `desiredDuration * size` for any other number of events will be further away
197 // from `totalDuration`. If the diff with `totalDuration` is the same, the higher value gets
198 // precedence.
199 val actualDiff = abs(totalDuration - desiredDuration * size)
200 val oneLessDiff = abs(totalDuration - desiredDuration * (size - 1))
201 val oneMoreDiff = abs(totalDuration - desiredDuration * (size + 1))
202 assertThat(actualDiff).isAtMost(oneLessDiff)
203 assertThat(actualDiff).isLessThan(oneMoreDiff)
204
205 // Check that the timestamps are within .5 of the unrounded splits
206 forEachIndexed { i, event ->
207 assertThat((event.relativeTime - t0).toFloat())
208 .isWithin(.5f)
209 .of(((i + 1) / size.toDouble() * totalDuration).toFloat())
210 }
211 }
212
213 private val MotionEvent.relativeTime
214 get() = eventTime - downTime
215
216 /**
217 * Checks if the subject is within [tolerance] of [f]. Shorthand for
218 * `isWithin([tolerance]).of([f])`.
219 */
isAlmostEqualTonull220 fun FloatSubject.isAlmostEqualTo(f: Float, tolerance: Float = 1e-3f) {
221 isWithin(tolerance).of(f)
222 }
223
224 /**
225 * Verifies that the [Offset] is equal to the given position with some tolerance. The default
226 * tolerance is 0.001.
227 */
isAlmostEqualTonull228 fun Offset.isAlmostEqualTo(position: Offset, tolerance: Float = 1e-3f, message: String? = null) {
229 if (message != null) {
230 assertWithMessage(message).that(x).isAlmostEqualTo(position.x, tolerance)
231 assertWithMessage(message).that(y).isAlmostEqualTo(position.y, tolerance)
232 } else {
233 assertThat(x).isAlmostEqualTo(position.x, tolerance)
234 assertThat(y).isAlmostEqualTo(position.y, tolerance)
235 }
236 }
237
238 /**
239 * Checks that the values are progressing in a monotonic direction between [a] and [b]. If [a] and
240 * [b] are equal, all values in the list should be that value too. The edges [a] and [b] allow a
241 * [tolerance] for floating point imprecision, which is by default `0.001`.
242 */
isMonotonicBetweennull243 fun List<Float>.isMonotonicBetween(a: Float, b: Float, tolerance: Float = 1e-3f) {
244 val expectedSign = sign(b - a)
245 if (expectedSign == 0f) {
246 forEach { assertThat(it).isAlmostEqualTo(a, tolerance) }
247 } else {
248 forEach { assertThat(it).isAlmostBetween(a, b, tolerance) }
249 zipWithNext { curr, next -> sign(next - curr) }
250 .forEach { if (it != 0f) assertThat(it).isEqualTo(expectedSign) }
251 }
252 }
253
Listnull254 fun List<Float>.assertSame(tolerance: Float = 0f) {
255 if (size <= 1) {
256 return
257 }
258 assertThat(minOrNull()!!).isWithin(2 * tolerance).of(maxOrNull()!!)
259 }
260
261 /**
262 * Checks that the float value is between [a] and [b], allowing a [tolerance] on either side. The
263 * order of [a] and [b] doesn't matter, the float value must be _between_ them. The default
264 * tolerance is `0.001`.
265 */
isAlmostBetweennull266 fun FloatSubject.isAlmostBetween(a: Float, b: Float, tolerance: Float = 1e-3f) {
267 if (a < b) {
268 isAtLeast(a - tolerance)
269 isAtMost(b + tolerance)
270 } else {
271 isAtLeast(b - tolerance)
272 isAtMost(a + tolerance)
273 }
274 }
275
assertIncreasingnull276 fun <E : Comparable<E>> List<E>.assertIncreasing() {
277 assertThat(this).isInOrder(Ordering.natural<E>())
278 }
279
assertDecreasingnull280 fun <E : Comparable<E>> List<E>.assertDecreasing() {
281 assertThat(this).isInOrder(Ordering.natural<E>().reverse<E>())
282 }
283