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