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