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 @file:JvmName("AccessibilityNodeInfoExt")
17
18 package androidx.test.uiautomator
19
20 import android.graphics.Bitmap
21 import android.graphics.Rect
22 import android.os.Build
23 import android.view.Display
24 import android.view.accessibility.AccessibilityNodeInfo
25 import androidx.test.uiautomator.internal.displayManager
26 import androidx.test.uiautomator.internal.findViews
27 import androidx.test.uiautomator.internal.notNull
28 import androidx.test.uiautomator.internal.takeScreenshotBitmap
29 import androidx.test.uiautomator.internal.takeViewNodeTree
30 import androidx.test.uiautomator.internal.uiDevice
31 import androidx.test.uiautomator.internal.waitForStableInternal
32
33 /**
34 * Returns the bounds of this node in screen coordinates. This method is a shortcut for
35 * [AccessibilityNodeInfo.getBoundsInScreen].
36 *
37 * @return a [Rect] with the bounds of this node within the screen limits.
38 */
<lambda>null39 public fun AccessibilityNodeInfo.boundsInScreen(): Rect = Rect().apply { getBoundsInScreen(this) }
40
41 /**
42 * Returns all the immediate children of this node. To return all the descendants (including
43 * children of children , children of children of children and so on use[
44 * [AccessibilityNodeInfo.descendants].
45 *
46 * @param block an optional predicate to filter the node children
47 * @return the list of children of this node, with the given filter if specified.
48 */
49 @JvmOverloads
childrennull50 public fun AccessibilityNodeInfo.children(
51 block: (AccessibilityNodeInfo) -> Boolean = { true }
<lambda>null52 ): List<AccessibilityNodeInfo> = (0 until childCount).mapNotNull { getChild(it) }.filter(block)
53
54 /**
55 * Performs a DFS on the accessibility tree starting from this node and returns all the descendants
56 * discovered recursively. Optionally a filter can be specified. Note that this differs from
57 * [AccessibilityNodeInfo.children()] because this will return also children of children, children
58 * of children of children and so on.
59 *
60 * @param filterBlock an optional predicate to filter the node children
61 * @return the list of all the descendants of this node, with the given filter if specified.
62 */
63 @JvmOverloads
descendantsnull64 public fun AccessibilityNodeInfo.descendants(
65 filterBlock: (AccessibilityNodeInfo) -> Boolean = { true }
66 ): List<AccessibilityNodeInfo> {
67 val collector = mutableListOf<AccessibilityNodeInfo>()
dfsnull68 fun dfs(node: AccessibilityNodeInfo) {
69 if (filterBlock(node)) collector.add(node)
70 node.children().forEach { dfs(it) }
71 }
72 dfs(this)
73 return collector
74 }
75
76 /**
77 * Returns all the siblings of the this node.
78 *
79 * @param filterBlock an optional predicate to filter the node siblings
80 * @return the list of the siblings of this node, with the given filter if specified.
81 */
82 @JvmOverloads
siblingsnull83 public fun AccessibilityNodeInfo.siblings(
84 filterBlock: (AccessibilityNodeInfo) -> Boolean = { true }
85 ): List<AccessibilityNodeInfo> = parent.children().filter(filterBlock).minus(this)
86
87 /**
88 * Takes a screenshot of the screen that contains this node and cuts only the area covered by it.
89 *
90 * @return a bitmap containing the image of this node.
91 */
takeScreenshotnull92 public fun AccessibilityNodeInfo.takeScreenshot(): Bitmap =
93 takeScreenshotBitmap(bounds = Rect().apply { getBoundsInScreen(this) })
94
95 /**
96 * Waits for the node to become stable. A node is considered stable when it and its descendants have
97 * not changed over an interval of time. Optionally also the node image can be checked. Internally
98 * it works checking periodically that the internal properties of the node have not changed.
99 *
100 * @param stableTimeoutMs a timeout for the wait operation, to ensure not waiting forever for
101 * stability.
102 * @param stableIntervalMs the interval during which the node should not be changing, in order to be
103 * considered stable.
104 * @param stablePollIntervalMs specifies how often the ui should be checked for changes.
105 * @param requireStableScreenshot specifies if also the bitmap of the node should not change over
106 * the specified [stableIntervalMs]. Note that this won't work with views that change constantly,
107 * like a video player.
108 * @return a [androidx.test.uiautomator.StableResult] containing the latest acquired view hierarchy
109 * and screenshot, and a flag indicating if the node was stable before timeout.
110 */
111 @JvmOverloads
waitForStablenull112 public fun AccessibilityNodeInfo.waitForStable(
113 stableTimeoutMs: Long = 3000,
114 stableIntervalMs: Long = 500,
115 stablePollIntervalMs: Long = 50,
116 requireStableScreenshot: Boolean = true,
117 ): StableResult {
118 val displayRect =
119 Rect().apply {
120 val displayId =
121 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) window.displayId
122 else Display.DEFAULT_DISPLAY
123 @Suppress("DEPRECATION") displayManager.getDisplay(displayId).getRectSize(this)
124 }
125
126 return waitForStableInternal(
127 stableTimeoutMs = stableTimeoutMs,
128 stablePollIntervalMs = stablePollIntervalMs,
129 stableIntervalMs = stableIntervalMs,
130 bitmapProvider = { if (requireStableScreenshot) takeScreenshot() else null },
131 rootViewNodeProvider = { takeViewNodeTree(root = this, displayRect = displayRect) }
132 )
133 }
134
135 /**
136 * Performs a DFS on the accessibility tree starting from this node and returns the first node
137 * matching the given [block]. The node is returned as an [UiObject2] that allows interacting with
138 * it. If the requested node doesn't exist, a [androidx.test.uiautomator.ViewNotFoundException] is
139 * thrown. Internally it works searching periodically every [pollIntervalMs].
140 *
141 * Example:
142 * ```kotlin
143 * node.onView { textAsString == "Search" }.click()
144 * ```
145 *
146 * @param timeoutMs a timeout to find the view that satisfies the given condition.
147 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
148 * @param block a block that specifies a condition on the node to find.
149 * @return a [UiObject2] from a node that matches the given [block] condition.
150 */
151 @JvmOverloads
onViewnull152 public fun AccessibilityNodeInfo.onView(
153 timeoutMs: Long = 10000,
154 pollIntervalMs: Long = 100,
155 block: AccessibilityNodeInfo.() -> (Boolean),
156 ): UiObject2 =
157 onViewOrNull(timeoutMs = timeoutMs, pollIntervalMs = pollIntervalMs, block = block)
158 .notNull(ViewNotFoundException())
159
160 /**
161 * Performs a DFS on the accessibility tree starting from this node and returns the first node
162 * matching the given [block]. The node is returned as an [UiObject2] that allows interacting with
163 * it. If the requested node doesn't exist, null is returned. Internally it works searching
164 * periodically every [pollIntervalMs].
165 *
166 * Example:
167 * ```kotlin
168 * node.onView { textAsString == "Search" }.click()
169 * ```
170 *
171 * @param timeoutMs a timeout to find the view that satisfies the given condition.
172 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
173 * @param block a block that specifies a condition on the node to find.
174 * @return a [UiObject2] from a node that matches the given [block] condition or null.
175 */
176 @JvmOverloads
177 public fun AccessibilityNodeInfo.onViewOrNull(
178 timeoutMs: Long = 10000,
179 pollIntervalMs: Long = 100,
180 block: AccessibilityNodeInfo.() -> (Boolean),
181 ): UiObject2? =
182 findViews(
183 shouldStop = { it.size == 1 },
184 block = block,
185 timeoutMs = timeoutMs,
186 pollIntervalMs = pollIntervalMs,
<lambda>null187 rootNodeBlock = { listOf(this) }
188 )
189 .firstOrNull()
190 ?.toUiObject2()
191
192 /**
193 * Performs a DFS on the accessibility tree starting from this node and returns all the nodes
194 * matching the given [block]. This method stops waiting as soon as a single node with the given
195 * condition is returned. The nodes returned are [UiObject2] that allow interacting with them.
196 * Internally it works searching periodically every [pollIntervalMs].
197 *
198 * Example:
199 * ```kotlin
200 * node.onViews { className == Button::class.java.name }
201 * ```
202 *
203 * If multiple nodes are expected but they appear at different times, it's recommended to call
204 * [androidx.test.uiautomator.waitForStable] before, to ensure any operation is complete.
205 *
206 * @param timeoutMs a timeout to find the view that satisfies the given condition.
207 * @param pollIntervalMs an interval to wait before rechecking the accessibility tree for updates.
208 * @param block a block that specifies a condition on the node to find.
209 * @return a list of [UiObject2] from nodes that matches the given [block] condition.
210 */
211 @JvmOverloads
onViewsnull212 public fun AccessibilityNodeInfo.onViews(
213 timeoutMs: Long = 10000,
214 pollIntervalMs: Long = 100,
215 block: AccessibilityNodeInfo.() -> (Boolean),
216 ): List<UiObject2> =
217 findViews(
218 timeoutMs = timeoutMs,
219 pollIntervalMs = pollIntervalMs,
220 shouldStop = { false },
221 block = block,
<lambda>null222 rootNodeBlock = { listOf(this) }
223 )
<lambda>null224 .mapNotNull { it.toUiObject2() }
225
toUiObject2null226 internal fun AccessibilityNodeInfo.toUiObject2(): UiObject2? =
227 UiObject2.create(uiDevice, BySelector(), this)
228
229 /**
230 * Returns this node's id without the full resource namespace, i.e. only the portion after the "/"
231 * character. If the view id is not specified, then it returns `null`.
232 */
233 public val AccessibilityNodeInfo.id: String?
234 get() = viewIdResourceName?.substringAfter("/")
235
236 /**
237 * Returns this node's text as a string. This should always be preferred to
238 * [AccessibilityNodeInfo.text] that instead returns a [CharSequence], that might be either a
239 * [String] or a [android.text.SpannableString]. If a text is not specified, then it returns `null`.
240 *
241 * Usage:
242 * ```kotlin
243 * onView { textAsString == "Some text" }.click()
244 * ```
245 */
246 public val AccessibilityNodeInfo.textAsString: String?
247 get() = text?.toString()
248
249 /**
250 * Returns whether this node class name is the same of the given class.
251 *
252 * Usage:
253 * ```kotlin
254 * onView { isClass(Button::class.java) }.click()
255 * ```
256 */
257 public fun AccessibilityNodeInfo.isClass(cls: Class<*>): Boolean = cls.name == this.className
258