1 /*
2 * Copyright 2025 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.test.uiautomator
18
19 import android.app.Instrumentation
20 import android.app.UiAutomation
21 import android.content.Intent
22 import android.view.accessibility.AccessibilityNodeInfo
23 import android.view.accessibility.AccessibilityWindowInfo
24 import androidx.test.platform.app.InstrumentationRegistry
25 import androidx.test.uiautomator.internal.AppManager
26 import androidx.test.uiautomator.watcher.ScopedUiWatcher
27 import androidx.test.uiautomator.watcher.WatcherRegistration
28
29 /**
30 * Main entry point for ui automator tests. It creates a [UiAutomatorTestScope] in which a test can
31 * be defined.
32 *
33 * Example:
34 * ```kotlin
35 * @Test
36 * fun myTest() = uiAutomator {
37 *
38 * startActivity(MyActivity::class.java)
39 *
40 * onView { id == "button" }.click()
41 *
42 * onView { id == "nested_elements" }
43 * .apply {
44 * onView { text == "First Level" }
45 * onView { text == "Second Level" }
46 * onView { text == "Third Level" }
47 * }
48 * }
49 * ```
50 *
51 * @param block A block containing the test to run within the [UiAutomatorTestScope].
52 */
uiAutomatornull53 public fun uiAutomator(block: UiAutomatorTestScope.() -> (Unit)) {
54 val scope = UiAutomatorTestScope()
55 block(scope)
56 scope.afterBlock()
57 }
58
59 /** A UiAutomator scope that allows to easily access UiAutomator api and utils class. */
60 public class UiAutomatorTestScope
61 internal constructor(
62 public val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
63 public val uiAutomation: UiAutomation = instrumentation.uiAutomation,
64 public val uiDevice: UiDevice = UiDevice.getInstance(instrumentation),
65 ) {
66
67 internal companion object {
68 internal const val TAG = "UiAutomatorTestScope"
69 }
70
71 private val appManager = AppManager(context = instrumentation.targetContext)
72 private val watcherRegistrations = mutableSetOf<WatcherRegistration>()
73
afterBlocknull74 internal fun afterBlock() {
75 watcherRegistrations.forEach { it.unregister() }
76 }
77
78 /**
79 * Registers a watcher for this [androidx.test.uiautomator.UiAutomatorTestScope] to handle
80 * unexpected UI elements. Internally this method uses the existing [UiDevice.registerWatcher]
81 * api. When the given [androidx.test.uiautomator.watcher.ScopedUiWatcher.isVisible] condition
82 * is satisfied, then the given [block] is executed. scope. This method returns a handler with
83 * the [WatcherRegistration] to unregister it before the block is complete. Note that this api
84 * helps with unexpected ui elements, such as system dialogs, and that for expected dialogs the
85 * [onView] api should be used.
86 *
87 * Usage:
88 * ```kotlin
89 * @Test fun myTest() = uiAutomator {
90 *
91 * // Registers a watcher for a permission dialog.
92 * watchFor(PermissionDialog) { clickAllow() }
93 *
94 * // Registers a watcher for a custom dialog and unregisters it.
95 * val registration = watchFor(MyDialog) { clickSomething() }
96 * // Do something...
97 * registration.unregister()
98 * }
99 * ```
100 *
101 * @param watcher the dialog to watch for.
102 * @param block a block to handle.
103 * @return the dialog registration.
104 */
watchFornull105 public fun <T> watchFor(
106 watcher: ScopedUiWatcher<T>,
107 block: T.() -> (Unit)
108 ): WatcherRegistration {
109 val id = watcher.toString()
110
111 uiDevice.registerWatcher(id) {
112 val visible = watcher.isVisible()
113 if (visible) block(watcher.scope())
114 visible
115 }
116
117 val registration =
118 object : WatcherRegistration {
119 override fun unregister() {
120 uiDevice.removeWatcher(id)
121 watcherRegistrations.remove(this)
122 }
123 }
124
125 watcherRegistrations.add(registration)
126 return registration
127 }
128
129 /**
130 * Performs a DFS on the accessibility tree starting from the root node in the active window and
131 * returns the first node matching the given [block]. The node is returned as an [UiObject2]
132 * that allows interacting with it. Internally it works searching periodically every
133 * [pollIntervalMs].
134 *
135 * Example:
136 * ```kotlin
137 * onView { textAsString == "Search" }.click()
138 * ```
139 *
140 * @param timeoutMs a timeout to find the view that satisfies the given condition.
141 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for
142 * updates.
143 * @param block a block that specifies a condition on the node to find.
144 * @return a [UiObject2] from a node that matches the given [block] condition.
145 */
146 @JvmOverloads
onViewnull147 public fun onView(
148 timeoutMs: Long = 10000,
149 pollIntervalMs: Long = 100,
150 block: AccessibilityNodeInfo.() -> (Boolean),
151 ): UiObject2 = uiDevice.onView(timeoutMs, pollIntervalMs, block)
152
153 /**
154 * Performs a DFS on the accessibility tree starting from the root node in the active window and
155 * returns the first node matching the given [block]. The node is returned as an [UiObject2]
156 * that allows interacting with it. Internally it works searching periodically every
157 * [pollIntervalMs].
158 *
159 * Example:
160 * ```kotlin
161 * onView { textAsString == "Search" }.click()
162 * ```
163 *
164 * @param timeoutMs a timeout to find the view that satisfies the given condition.
165 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for
166 * updates.
167 * @param block a block that specifies a condition on the node to find.
168 * @return a [UiObject2] from a node that matches the given [block] condition or null.
169 */
170 @JvmOverloads
171 public fun onViewOrNull(
172 timeoutMs: Long = 10000,
173 pollIntervalMs: Long = 100,
174 block: AccessibilityNodeInfo.() -> (Boolean),
175 ): UiObject2? = uiDevice.onViewOrNull(timeoutMs, pollIntervalMs, block)
176
177 /**
178 * Performs a DFS on the accessibility tree starting from the root node in the active window and
179 * returns the first node matching the given [block]. The node is returned as an [UiObject2]
180 * that allows interacting with it. Internally it works searching periodically every
181 * [pollIntervalMs].
182 *
183 * Example:
184 * ```kotlin
185 * node.onViews { isClass(Button::class.java) }
186 * ```
187 *
188 * @param timeoutMs a timeout to find the view that satisfies the given condition.
189 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for
190 * updates.
191 * @param block a block that specifies a condition on the node to find.
192 * @return a list of [UiObject2] from nodes that matches the given [block] condition.
193 */
194 @JvmOverloads
195 public fun onViews(
196 timeoutMs: Long = 10000,
197 pollIntervalMs: Long = 100,
198 block: AccessibilityNodeInfo.() -> (Boolean),
199 ): List<UiObject2> = uiDevice.onViews(timeoutMs, pollIntervalMs, block)
200
201 /**
202 * Waits for an application to become visible. Note that internally it checks if an
203 * accessibility node with the given [appPackageName] exists in the accessibility tree.
204 *
205 * @param appPackageName the package name of the app to wait for. By default is the target app
206 * package name.
207 * @param timeoutMs a timeout for the app to become visible.
208 * @return whether the app became visible in the given timeout.
209 */
210 @JvmOverloads
211 public fun waitForAppToBeVisible(
212 appPackageName: String = instrumentation.targetContext.packageName,
213 timeoutMs: Long = 10000L,
214 ): Boolean =
215 uiDevice.waitForAppToBeVisible(
216 appPackageName = appPackageName,
217 timeoutMs = timeoutMs,
218 )
219
220 /**
221 * Types the given [text] string simulating key press through [Instrumentation.sendKeySync].
222 * This is similar to tapping the keys on a virtual keyboard and will trigger the same listeners
223 * in the target app, as opposed to [AccessibilityNodeInfo.setText] that programmaticaly sets
224 * the given text in the target node.
225 *
226 * @param text the text to type.
227 */
228 public fun type(text: String): Unit = uiDevice.type(text)
229
230 /**
231 * Similar to [type] but presses the delete key for the given [count] times.
232 *
233 * @param count how many times the press delete key should be pressed.
234 */
235 public fun pressDelete(count: Int): Unit = uiDevice.pressDelete(count)
236
237 /** Press the enter key. */
238 public fun pressEnter(): Boolean = uiDevice.pressEnter()
239
240 /** Press the back key. */
241 public fun pressBack(): Boolean = uiDevice.pressBack()
242
243 /** Press the home key. */
244 public fun pressHome(): Boolean = uiDevice.pressHome()
245
246 /** Returns all the windows on all the displays. */
247 public fun windows(): List<AccessibilityWindowInfo> = uiDevice.windows()
248
249 /** Returns all the window roots on all the displays. */
250 public fun windowRoots(): List<AccessibilityNodeInfo> = uiDevice.windowRoots
251
252 /**
253 * Returns the active window. Note that calling this method after [startApp], [startActivity] or
254 * [startIntent] without waiting for the app to be visible, will return the active window at the
255 * time of starting the app, i.e. the launcher if starting from there.
256 */
257 public fun activeWindow(): AccessibilityWindowInfo = activeWindowRoot().window
258
259 /**
260 * Returns the active window root node. Note that calling this method after [startApp],
261 * [startActivity] or [startIntent] without waiting for the app to be visible, will return the
262 * active window root at the time of starting the app, i.e. the root of the launcher if starting
263 * from there.
264 */
265 public fun activeWindowRoot(): AccessibilityNodeInfo = uiDevice.waitForRootInActiveWindow()
266
267 /** Starts the instrumentation test target app using the target app package name. */
268 public fun startApp(): Unit = startApp(instrumentation.targetContext.packageName)
269
270 /**
271 * Starts the app with the given [packageName].
272 *
273 * @param packageName the package name of the app to start
274 */
275 public fun startApp(packageName: String): Unit = appManager.startApp(packageName = packageName)
276
277 /**
278 * Starts an activity with the given [packageName] and [activityName].
279 *
280 * @param packageName the app package name of the activity to start.
281 * @param activityName the name of the activity to start.
282 */
283 public fun startActivity(
284 packageName: String,
285 activityName: String,
286 ): Unit = appManager.startActivity(packageName = packageName, activityName = activityName)
287
288 /**
289 * Starts an activity with the given class.
290 *
291 * @param clazz the class of the activity to start.
292 */
293 public fun startActivity(clazz: Class<*>): Unit = appManager.startActivity(clazz = clazz)
294
295 /**
296 * Starts the given [intent].
297 *
298 * @param intent an intent to start
299 */
300 public fun startIntent(intent: Intent): Unit = appManager.startIntent(intent = intent)
301
302 /** Clears the instrumentation test target app data. */
303 public fun clearAppData(): Unit = appManager.clearAppData()
304 }
305