1 /*
<lambda>null2  * Copyright 2022 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
18 
19 import androidx.compose.ui.input.key.Key
20 import androidx.compose.ui.test.internal.JvmDefaultWithCompatibility
21 
22 /** Default duration of a key press in milliseconds (duration between key down and key up). */
23 private const val DefaultKeyPressDurationMillis = 50L
24 
25 /** Default duration of the pause between sequential key presses in milliseconds. */
26 private const val DefaultPauseDurationBetweenKeyPressesMillis = 50L
27 
28 /**
29  * The receiver scope of the key input injection lambda from [performKeyInput].
30  *
31  * All sequences and patterns of key input can be expressed using the two fundamental methods of
32  * this API - [keyDown] and [keyUp]. All other injection functions are provided as abstractions
33  * built on top of these two methods in order to improve test code readability/maintainability and
34  * decrease development time.
35  *
36  * The entire event injection state is shared between all `perform.*Input` methods, meaning you can
37  * continue an unfinished key input sequence in a subsequent invocation of [performKeyInput] or
38  * [performMultiModalInput].
39  *
40  * All events sent by these methods are batched together and sent as a whole after [performKeyInput]
41  * has executed its code block.
42  *
43  * When a key is held down - i.e. the virtual clock is forwarded whilst the key is pressed down,
44  * repeat key down events will be sent. In a fashion consistent with Android's implementation, the
45  * first repeat key event will be sent after a key has been held down for 500ms. Subsequent repeat
46  * events will be sent at 50ms intervals, until the key is released or another key is pressed down.
47  *
48  * The sending of repeat key events is handled as an implicit side-effect of [advanceEventTime],
49  * which is called within the injection scope. As such, no repeat key events will be sent if
50  * [MainTestClock.advanceTimeBy] is used to advance the time.
51  *
52  * @see InjectionScope
53  */
54 @JvmDefaultWithCompatibility
55 interface KeyInjectionScope : InjectionScope {
56 
57     /**
58      * Indicates whether caps lock is on or not.
59      *
60      * Note that this reflects the state of the injected input only, it does not correspond to the
61      * state of an actual keyboard attached to the device on which a test is run
62      */
63     val isCapsLockOn: Boolean
64 
65     /**
66      * Indicates whether num lock is on or not.
67      *
68      * Note that this reflects the state of the injected input only, it does not correspond to the
69      * state of an actual keyboard attached to the device on which a test is run
70      */
71     val isNumLockOn: Boolean
72 
73     /**
74      * Indicates whether scroll lock is on or not.
75      *
76      * Note that this reflects the state of the injected input only, it does not correspond to the
77      * state of an actual keyboard attached to the device on which a test is run
78      */
79     val isScrollLockOn: Boolean
80 
81     /**
82      * Sends a key down event for the given [key].
83      *
84      * If the given key is already down, an [IllegalStateException] will be thrown.
85      *
86      * @param key The key to be pressed down.
87      */
88     fun keyDown(key: Key)
89 
90     /**
91      * Sends a key up event for the given [key].
92      *
93      * If the given key is already up, an [IllegalStateException] will be thrown.
94      *
95      * @param key The key to be released.
96      */
97     fun keyUp(key: Key)
98 
99     /**
100      * Checks if the given [key] is down.
101      *
102      * @param key The key to be checked.
103      * @return true if the given [key] is pressed down, false otherwise.
104      */
105     fun isKeyDown(key: Key): Boolean
106 }
107 
108 internal class KeyInjectionScopeImpl(private val baseScope: MultiModalInjectionScopeImpl) :
<lambda>null109     KeyInjectionScope, InjectionScope by baseScope {
110     private val inputDispatcher
111         get() = baseScope.inputDispatcher
112 
113     override val isCapsLockOn: Boolean
114         get() = inputDispatcher.isCapsLockOn
115 
116     override val isNumLockOn: Boolean
117         get() = inputDispatcher.isNumLockOn
118 
119     override val isScrollLockOn: Boolean
120         get() = inputDispatcher.isScrollLockOn
121 
122     // TODO(b/233186704) Find out why KeyEvents not registered when injected together in batches.
123     override fun keyDown(key: Key) {
124         inputDispatcher.enqueueKeyDown(key)
125         inputDispatcher.flush()
126     }
127 
128     override fun keyUp(key: Key) {
129         inputDispatcher.enqueueKeyUp(key)
130         inputDispatcher.flush()
131     }
132 
133     override fun isKeyDown(key: Key): Boolean = inputDispatcher.isKeyDown(key)
134 }
135 
136 /**
137  * Holds down the given [key] for the given [pressDurationMillis] by sending a key down event,
138  * advancing the event time and sending a key up event.
139  *
140  * If the given key is already down, an [IllegalStateException] will be thrown.
141  *
142  * @param key The key to be pressed down.
143  * @param pressDurationMillis Duration of press in milliseconds.
144  */
pressKeynull145 fun KeyInjectionScope.pressKey(
146     key: Key,
147     pressDurationMillis: Long = DefaultKeyPressDurationMillis
148 ) {
149     keyDown(key)
150     advanceEventTime(pressDurationMillis)
151     keyUp(key)
152 }
153 
154 /**
155  * Executes the keyboard sequence specified in the given [block], whilst holding down the given
156  * [key]. This key must not be used within the [block].
157  *
158  * If the given [key] is already down, an [IllegalStateException] will be thrown.
159  *
160  * @param key The key to be held down during injection of the [block].
161  * @param block Sequence of KeyInjectionScope methods to be injected with the given key down.
162  */
withKeyDownnull163 fun KeyInjectionScope.withKeyDown(key: Key, block: KeyInjectionScope.() -> Unit) {
164     keyDown(key)
165     try {
166         block.invoke(this)
167     } finally {
168         keyUp(key)
169     }
170 }
171 
172 /**
173  * Executes the keyboard sequence specified in the given [block], whilst holding down the each of
174  * the given [keys]. Each of the [keys] will be pressed down and released simultaneously. These keys
175  * must not be used within the [block].
176  *
177  * If any of the given [keys] are already down, an [IllegalStateException] will be thrown.
178  *
179  * @param keys List of keys to be held down during injection of the [block].
180  * @param block Sequence of KeyInjectionScope methods to be injected with the given keys down.
181  */
182 // TODO(b/234011835): Refactor this and all functions that take List<Keys> to use vararg instead.
KeyInjectionScopenull183 fun KeyInjectionScope.withKeysDown(keys: List<Key>, block: KeyInjectionScope.() -> Unit) {
184     keys.forEach { keyDown(it) }
185     try {
186         block.invoke(this)
187     } finally {
188         keys.forEach { keyUp(it) }
189     }
190 }
191 
192 /**
193  * Executes the keyboard sequence specified in the given [block], in between presses to the given
194  * [key]. This key can also be used within the [block], as long as it is not down at the end of the
195  * block.
196  *
197  * If the given [key] is already down, an [IllegalStateException] will be thrown.
198  *
199  * @param key The key to be toggled around the injection of the [block].
200  * @param block Sequence of KeyInjectionScope methods to be injected with the given key down.
201  */
withKeyTogglednull202 fun KeyInjectionScope.withKeyToggled(key: Key, block: KeyInjectionScope.() -> Unit) {
203     pressKey(key)
204     try {
205         block.invoke(this)
206     } finally {
207         pressKey(key)
208     }
209 }
210 
211 /**
212  * Executes the keyboard sequence specified in the given [block], in between presses to the given
213  * [keys]. Each of the [keys] will be toggled simultaneously.These keys can also be used within the
214  * [block], as long as they are not down at the end of the block.
215  *
216  * If any of the given [keys] are already down, an [IllegalStateException] will be thrown.
217  *
218  * @param keys The keys to be toggled around the injection of the [block].
219  * @param block Sequence of KeyInjectionScope methods to be injected with the given keys down.
220  */
KeyInjectionScopenull221 fun KeyInjectionScope.withKeysToggled(keys: List<Key>, block: KeyInjectionScope.() -> Unit) {
222     pressKeys(keys)
223     try {
224         block.invoke(this)
225     } finally {
226         pressKeys(keys)
227     }
228 }
229 
230 /**
231  * Verifies whether the function key is down.
232  *
233  * @return true if the function key is currently down, false otherwise.
234  */
235 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
236 val KeyInjectionScope.isFnDown: Boolean
237     get() = isKeyDown(Key.Function)
238 
239 /**
240  * Verifies whether either of the control keys are down.
241  *
242  * @return true if a control key is currently down, false otherwise.
243  */
244 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
245 val KeyInjectionScope.isCtrlDown: Boolean
246     get() = isKeyDown(Key.CtrlLeft) || isKeyDown(Key.CtrlRight)
247 
248 /**
249  * Verifies whether either of the alt keys are down.
250  *
251  * @return true if an alt key is currently down, false otherwise.
252  */
253 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
254 val KeyInjectionScope.isAltDown: Boolean
255     get() = isKeyDown(Key.AltLeft) || isKeyDown(Key.AltRight)
256 
257 /**
258  * Verifies whether either of the meta keys are down.
259  *
260  * @return true if a meta key is currently down, false otherwise.
261  */
262 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
263 val KeyInjectionScope.isMetaDown: Boolean
264     get() = isKeyDown(Key.MetaLeft) || isKeyDown(Key.MetaRight)
265 
266 /**
267  * Verifies whether either of the shift keys are down.
268  *
269  * @return true if a shift key is currently down, false otherwise.
270  */
271 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
272 val KeyInjectionScope.isShiftDown: Boolean
273     get() = isKeyDown(Key.ShiftLeft) || isKeyDown(Key.ShiftRight)
274 
275 /**
276  * Holds down the key each of the given [keys] for [DefaultKeyPressDurationMillis] in sequence, with
277  * [DefaultPauseDurationBetweenKeyPressesMillis] between each press.
278  *
279  * If one of the keys is already down, an [IllegalStateException] will be thrown.
280  *
281  * @param keys The list of keys to be pressed down.
282  */
pressKeysnull283 private fun KeyInjectionScope.pressKeys(keys: List<Key>) =
284     keys.forEachIndexed { idx: Int, key: Key ->
285         if (idx != 0) advanceEventTime(DefaultPauseDurationBetweenKeyPressesMillis)
286         pressKey(key, DefaultKeyPressDurationMillis)
287     }
288