1 /*
2 * 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
18
19 import androidx.compose.ui.geometry.Rect
20 import androidx.compose.ui.semantics.ProgressBarRangeInfo
21 import androidx.compose.ui.semantics.SemanticsNode
22 import androidx.compose.ui.semantics.SemanticsProperties
23
24 /**
25 * Asserts that the current semantics node is displayed on screen.
26 *
27 * Specifically, the node must be composed, placed and at least a portion of its bounds must be
28 * visible on screen after clipping is applied.
29 *
30 * Throws [AssertionError] if the node is not displayed.
31 */
SemanticsNodeInteractionnull32 fun SemanticsNodeInteraction.assertIsDisplayed(): SemanticsNodeInteraction {
33 if (!isDisplayed()) {
34 throw AssertionError("Assert failed: The component is not displayed!")
35 }
36 return this
37 }
38
39 /**
40 * Asserts that the current semantics node is not displayed on screen.
41 *
42 * Throws [AssertionError] if the node is displayed.
43 */
SemanticsNodeInteractionnull44 fun SemanticsNodeInteraction.assertIsNotDisplayed(): SemanticsNodeInteraction {
45 if (!isNotDisplayed()) {
46 throw AssertionError("Assert failed: The component is displayed!")
47 }
48 return this
49 }
50
51 /**
52 * Asserts that the current semantics node is enabled.
53 *
54 * Throws [AssertionError] if the node is not enabled or does not define the property at all.
55 */
SemanticsNodeInteractionnull56 fun SemanticsNodeInteraction.assertIsEnabled(): SemanticsNodeInteraction = assert(isEnabled())
57
58 /**
59 * Asserts that the current semantics node is not enabled.
60 *
61 * Throws [AssertionError] if the node is enabled or does not defined the property at all.
62 */
63 fun SemanticsNodeInteraction.assertIsNotEnabled(): SemanticsNodeInteraction = assert(isNotEnabled())
64
65 /**
66 * Asserts that the current semantics node is checked.
67 *
68 * Throws [AssertionError] if the node is not unchecked, indeterminate, or not toggleable.
69 */
70 fun SemanticsNodeInteraction.assertIsOn(): SemanticsNodeInteraction = assert(isOn())
71
72 /**
73 * Asserts that the current semantics node is unchecked.
74 *
75 * Throws [AssertionError] if the node is checked, indeterminate, or not toggleable.
76 */
77 fun SemanticsNodeInteraction.assertIsOff(): SemanticsNodeInteraction = assert(isOff())
78
79 /**
80 * Asserts that the current semantics node is selected.
81 *
82 * Throws [AssertionError] if the node is unselected or not selectable.
83 */
84 fun SemanticsNodeInteraction.assertIsSelected(): SemanticsNodeInteraction = assert(isSelected())
85
86 /**
87 * Asserts that the current semantics node is not selected.
88 *
89 * Throws [AssertionError] if the node is selected or not selectable.
90 */
91 fun SemanticsNodeInteraction.assertIsNotSelected(): SemanticsNodeInteraction =
92 assert(isNotSelected())
93
94 /**
95 * Asserts that the current semantics node is toggleable.
96 *
97 * Throws [AssertionError] if the node is not toggleable.
98 */
99 fun SemanticsNodeInteraction.assertIsToggleable(): SemanticsNodeInteraction = assert(isToggleable())
100
101 /**
102 * Asserts that the current semantics node is selectable.
103 *
104 * Throws [AssertionError] if the node is not selectable.
105 */
106 fun SemanticsNodeInteraction.assertIsSelectable(): SemanticsNodeInteraction = assert(isSelectable())
107
108 /**
109 * Asserts that the current semantics node has a focus.
110 *
111 * Throws [AssertionError] if the node is not in the focus or does not defined the property at all.
112 */
113 fun SemanticsNodeInteraction.assertIsFocused(): SemanticsNodeInteraction = assert(isFocused())
114
115 /**
116 * Asserts that the current semantics node does not have a focus.
117 *
118 * Throws [AssertionError] if the node is in the focus or does not defined the property at all.
119 */
120 fun SemanticsNodeInteraction.assertIsNotFocused(): SemanticsNodeInteraction = assert(isNotFocused())
121
122 /**
123 * Asserts that the node's content description contains exactly the given [values] and nothing else.
124 *
125 * Note that in merged semantics tree there can be a list of content descriptions that got merged
126 * from the child nodes. Typically an accessibility tooling will decide based on its heuristics
127 * which ones to announce.
128 *
129 * Throws [AssertionError] if the node's descriptions don't contain all items from [values], or if
130 * the descriptions contain extra items that are not in [values].
131 *
132 * @param values List of values to match (the order does not matter)
133 * @see SemanticsProperties.ContentDescription
134 */
135 fun SemanticsNodeInteraction.assertContentDescriptionEquals(
136 vararg values: String
137 ): SemanticsNodeInteraction = assert(hasContentDescriptionExactly(*values))
138
139 /**
140 * Asserts that the node's content description contains the given [value].
141 *
142 * Note that in merged semantics tree there can be a list of content descriptions that got merged
143 * from the child nodes. Typically an accessibility tooling will decide based on its heuristics
144 * which ones to announce.
145 *
146 * Throws [AssertionError] if the node's value does not contain `value`, or if the node has no value
147 *
148 * @param value Value to match as one of the items in the list of content descriptions.
149 * @param substring Whether this can be satisfied as a substring match of an item in the list of
150 * descriptions.
151 * @param ignoreCase Whether case should be ignored.
152 * @see SemanticsProperties.ContentDescription
153 */
154 fun SemanticsNodeInteraction.assertContentDescriptionContains(
155 value: String,
156 substring: Boolean = false,
157 ignoreCase: Boolean = false
158 ): SemanticsNodeInteraction =
159 assert(hasContentDescription(value, substring = substring, ignoreCase = ignoreCase))
160
161 /**
162 * Asserts that the node's text contains exactly the given [values] and nothing else.
163 *
164 * This will also search in [SemanticsProperties.EditableText] by default.
165 *
166 * Note that in merged semantics tree there can be a list of text items that got merged from the
167 * child nodes. Typically an accessibility tooling will decide based on its heuristics which ones to
168 * use.
169 *
170 * Throws [AssertionError] if the node's text values don't contain all items from [values], or if
171 * the text values contain extra items that are not in [values].
172 *
173 * @param values List of values to match (the order does not matter)
174 * @param includeEditableText Whether to also assert against the editable text.
175 * @see SemanticsProperties.ContentDescription
176 */
177 fun SemanticsNodeInteraction.assertTextEquals(
178 vararg values: String,
179 includeEditableText: Boolean = true
180 ): SemanticsNodeInteraction =
181 assert(hasTextExactly(*values, includeEditableText = includeEditableText))
182
183 /**
184 * Asserts that the node's text contains the given [value].
185 *
186 * This will also search in [SemanticsProperties.EditableText] and [SemanticsProperties.InputText].
187 *
188 * Note that in merged semantics tree there can be a list of text items that got merged from the
189 * child nodes. Typically an accessibility tooling will decide based on its heuristics which ones to
190 * use.
191 *
192 * Throws [AssertionError] if the node's value does not contain `value`, or if the node has no value
193 *
194 * @param value Value to match as one of the items in the list of text values.
195 * @param substring Whether this can be satisfied as a substring match of an item in the list of
196 * text.
197 * @param ignoreCase Whether case should be ignored.
198 * @see SemanticsProperties.Text
199 */
200 fun SemanticsNodeInteraction.assertTextContains(
201 value: String,
202 substring: Boolean = false,
203 ignoreCase: Boolean = false
204 ): SemanticsNodeInteraction = assert(hasText(value, substring = substring, ignoreCase = ignoreCase))
205
206 /**
207 * Asserts the node's value equals the given value.
208 *
209 * For further details please check [SemanticsProperties.StateDescription]. Throws [AssertionError]
210 * if the node's value is not equal to `value`, or if the node has no value
211 */
212 fun SemanticsNodeInteraction.assertValueEquals(value: String): SemanticsNodeInteraction =
213 assert(hasStateDescription(value))
214
215 /**
216 * Asserts the node's range info equals the given value.
217 *
218 * For further details please check [SemanticsProperties.ProgressBarRangeInfo]. Throws
219 * [AssertionError] if the node's value is not equal to `value`, or if the node has no value
220 */
221 fun SemanticsNodeInteraction.assertRangeInfoEquals(
222 value: ProgressBarRangeInfo
223 ): SemanticsNodeInteraction = assert(hasProgressBarRangeInfo(value))
224
225 /**
226 * Asserts that the current semantics node has a click action.
227 *
228 * Throws [AssertionError] if the node is doesn't have a click action.
229 */
230 fun SemanticsNodeInteraction.assertHasClickAction(): SemanticsNodeInteraction =
231 assert(hasClickAction())
232
233 /**
234 * Asserts that the current semantics node has doesn't have a click action.
235 *
236 * Throws [AssertionError] if the node has a click action.
237 */
238 fun SemanticsNodeInteraction.assertHasNoClickAction(): SemanticsNodeInteraction =
239 assert(hasNoClickAction())
240
241 /**
242 * Asserts that the provided [matcher] is satisfied for this node.
243 *
244 * @param matcher Matcher to verify.
245 * @param messagePrefixOnError Prefix to be put in front of an error that gets thrown in case this
246 * assert fails. This can be helpful in situations where this assert fails as part of a bigger
247 * operation that used this assert as a precondition check.
248 * @throws AssertionError if the matcher does not match or the node can no longer be found.
249 */
250 fun SemanticsNodeInteraction.assert(
251 matcher: SemanticsMatcher,
252 messagePrefixOnError: (() -> String)? = null
253 ): SemanticsNodeInteraction {
254 var errorMessageOnFail = "Failed to assert the following: (${matcher.description})"
255 if (messagePrefixOnError != null) {
256 errorMessageOnFail = messagePrefixOnError() + "\n" + errorMessageOnFail
257 }
258 val node = fetchSemanticsNode(errorMessageOnFail)
259 if (!matcher.matches(node)) {
260 throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, selector, node))
261 }
262 return this
263 }
264
265 /**
266 * Asserts that this collection of nodes is equal to the given [expectedSize].
267 *
268 * Provides a detailed error message on failure.
269 *
270 * @throws AssertionError if the size is not equal to [expectedSize]
271 */
assertCountEqualsnull272 fun SemanticsNodeInteractionCollection.assertCountEquals(
273 expectedSize: Int
274 ): SemanticsNodeInteractionCollection {
275 val errorOnFail = "Failed to assert count of nodes."
276 val matchedNodes = fetchSemanticsNodes(atLeastOneRootRequired = expectedSize > 0, errorOnFail)
277 if (matchedNodes.size != expectedSize) {
278 throw AssertionError(
279 buildErrorMessageForCountMismatch(
280 errorMessage = errorOnFail,
281 selector = selector,
282 foundNodes = matchedNodes,
283 expectedCount = expectedSize
284 )
285 )
286 }
287 return this
288 }
289
290 /**
291 * Asserts that this collection contains at least one element that satisfies the given [matcher].
292 *
293 * @param matcher Matcher that has to be satisfied by at least one of the nodes in the collection.
294 * @throws AssertionError if not at least one matching node was node.
295 */
assertAnynull296 fun SemanticsNodeInteractionCollection.assertAny(
297 matcher: SemanticsMatcher
298 ): SemanticsNodeInteractionCollection {
299 val errorOnFail = "Failed to assertAny(${matcher.description})"
300 val nodes = fetchSemanticsNodes(errorMessageOnFail = errorOnFail)
301 if (nodes.isEmpty()) {
302 throw AssertionError(buildErrorMessageForAtLeastOneNodeExpected(errorOnFail, selector))
303 }
304 if (!matcher.matchesAny(nodes)) {
305 throw AssertionError(buildErrorMessageForAssertAnyFail(selector, nodes, matcher))
306 }
307 return this
308 }
309
310 /**
311 * Asserts that all the nodes in this collection satisfy the given [matcher].
312 *
313 * This passes also for empty collections.
314 *
315 * @param matcher Matcher that has to be satisfied by all the nodes in the collection.
316 * @throws AssertionError if the collection contains at least one element that does not satisfy the
317 * given matcher.
318 */
SemanticsNodeInteractionCollectionnull319 fun SemanticsNodeInteractionCollection.assertAll(
320 matcher: SemanticsMatcher
321 ): SemanticsNodeInteractionCollection {
322 val errorOnFail = "Failed to assertAll(${matcher.description})"
323 val nodes = fetchSemanticsNodes(errorMessageOnFail = errorOnFail)
324
325 val violations = mutableListOf<SemanticsNode>()
326 nodes.forEach {
327 if (!matcher.matches(it)) {
328 violations.add(it)
329 }
330 }
331 if (violations.isNotEmpty()) {
332 throw AssertionError(buildErrorMessageForAssertAllFail(selector, violations, matcher))
333 }
334 return this
335 }
336
337 /**
338 * Returns true if the matched node is displayed on screen.
339 *
340 * Specifically, the node must be composed, placed and at least a portion of its bounds must be
341 * visible on screen after clipping is applied. If no matching node is found, returns false. If
342 * multiple nodes match, throws an [AssertionError].
343 *
344 * @sample androidx.compose.ui.test.samples.waitForDisplayed
345 * @throws AssertionError If multiple nodes match this [SemanticsNodeInteraction].
346 */
SemanticsNodeInteractionnull347 fun SemanticsNodeInteraction.isDisplayed(): Boolean = checkIsDisplayed(assertIsFullyVisible = false)
348
349 /**
350 * Asserts that the current semantics node is not displayed on screen.
351 *
352 * If no matching node is found, returns true. If multiple nodes match, throws an [AssertionError].
353 *
354 * @sample androidx.compose.ui.test.samples.waitForNotDisplayed
355 * @throws AssertionError If multiple nodes match this [SemanticsNodeInteraction].
356 */
357 fun SemanticsNodeInteraction.isNotDisplayed(): Boolean =
358 !checkIsDisplayed(assertIsFullyVisible = true)
359
360 @Suppress("DocumentExceptions")
361 internal expect fun SemanticsNodeInteraction.checkIsDisplayed(
362 assertIsFullyVisible: Boolean
363 ): Boolean
364
365 internal expect fun SemanticsNode.clippedNodeBoundsInWindow(): Rect
366
367 internal expect fun SemanticsNode.isInScreenBounds(assertIsFullyVisible: Boolean): Boolean
368