1 /*
<lambda>null2  * 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.semantics.ProgressBarRangeInfo
20 import androidx.compose.ui.semantics.SemanticsActions
21 import androidx.compose.ui.semantics.SemanticsNode
22 import androidx.compose.ui.semantics.SemanticsProperties
23 import androidx.compose.ui.semantics.SemanticsProperties.EditableText
24 import androidx.compose.ui.semantics.SemanticsProperties.InputText
25 import androidx.compose.ui.semantics.SemanticsProperties.Text
26 import androidx.compose.ui.semantics.SemanticsPropertyKey
27 import androidx.compose.ui.semantics.getOrNull
28 import androidx.compose.ui.state.ToggleableState
29 import androidx.compose.ui.text.input.ImeAction
30 import androidx.compose.ui.util.fastAny
31 import kotlin.js.JsName
32 
33 /**
34  * Returns whether the node is enabled.
35  *
36  * @see SemanticsProperties.Disabled
37  */
38 fun isEnabled(): SemanticsMatcher =
39     SemanticsMatcher("is enabled") { SemanticsProperties.Disabled !in it.config }
40 
41 /**
42  * Returns whether the node is not enabled.
43  *
44  * @see SemanticsProperties.Disabled
45  */
isNotEnablednull46 fun isNotEnabled(): SemanticsMatcher =
47     SemanticsMatcher("is not enabled") { SemanticsProperties.Disabled in it.config }
48 
49 /**
50  * Return whether the node is checkable.
51  *
52  * @see SemanticsProperties.ToggleableState
53  */
isToggleablenull54 fun isToggleable(): SemanticsMatcher = hasKey(SemanticsProperties.ToggleableState)
55 
56 /**
57  * Returns whether the node is toggled.
58  *
59  * @see SemanticsProperties.ToggleableState
60  */
61 fun isOn(): SemanticsMatcher =
62     SemanticsMatcher.expectValue(SemanticsProperties.ToggleableState, ToggleableState.On)
63 
64 /**
65  * Returns whether the node is not toggled.
66  *
67  * @see SemanticsProperties.ToggleableState
68  */
69 fun isOff(): SemanticsMatcher =
70     SemanticsMatcher.expectValue(SemanticsProperties.ToggleableState, ToggleableState.Off)
71 
72 /**
73  * Return whether the node is selectable.
74  *
75  * @see SemanticsProperties.Selected
76  */
77 fun isSelectable(): SemanticsMatcher = hasKey(SemanticsProperties.Selected)
78 
79 /**
80  * Returns whether the node is selected.
81  *
82  * @see SemanticsProperties.Selected
83  */
84 fun isSelected(): SemanticsMatcher =
85     SemanticsMatcher.expectValue(SemanticsProperties.Selected, true)
86 
87 /**
88  * Returns whether the node is not selected.
89  *
90  * @see SemanticsProperties.Selected
91  */
92 fun isNotSelected(): SemanticsMatcher =
93     SemanticsMatcher.expectValue(SemanticsProperties.Selected, false)
94 
95 /**
96  * Return whether the node is able to receive focus
97  *
98  * @see SemanticsProperties.Focused
99  */
100 fun isFocusable(): SemanticsMatcher = hasKey(SemanticsProperties.Focused)
101 
102 /**
103  * Return whether the node is not able to receive focus.
104  *
105  * @see SemanticsProperties.Focused
106  */
107 fun isNotFocusable(): SemanticsMatcher = SemanticsMatcher.keyNotDefined(SemanticsProperties.Focused)
108 
109 /**
110  * Returns whether the node is focused.
111  *
112  * @see SemanticsProperties.Focused
113  */
114 fun isFocused(): SemanticsMatcher = SemanticsMatcher.expectValue(SemanticsProperties.Focused, true)
115 
116 /**
117  * Returns whether the node is not focused.
118  *
119  * @see SemanticsProperties.Focused
120  */
121 fun isNotFocused(): SemanticsMatcher =
122     SemanticsMatcher.expectValue(SemanticsProperties.Focused, false)
123 
124 /**
125  * Return whether the node has a semantics click action defined.
126  *
127  * @see SemanticsActions.OnClick
128  */
129 fun hasClickAction(): SemanticsMatcher = hasKey(SemanticsActions.OnClick)
130 
131 /**
132  * Return whether the node has no semantics click action defined.
133  *
134  * @see SemanticsActions.OnClick
135  */
136 fun hasNoClickAction(): SemanticsMatcher = SemanticsMatcher.keyNotDefined(SemanticsActions.OnClick)
137 
138 /**
139  * Return whether the node has a semantics scrollable action defined.
140  *
141  * @see SemanticsActions.ScrollBy
142  */
143 fun hasScrollAction(): SemanticsMatcher = hasKey(SemanticsActions.ScrollBy)
144 
145 /**
146  * Return whether the node has no semantics scrollable action defined.
147  *
148  * @see SemanticsActions.ScrollBy
149  */
150 fun hasNoScrollAction(): SemanticsMatcher =
151     SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy)
152 
153 /**
154  * Returns whether the node's content description contains the given [value].
155  *
156  * Note that in merged semantics tree there can be a list of content descriptions that got merged
157  * from the child nodes. Typically an accessibility tooling will decide based on its heuristics
158  * which ones to announce.
159  *
160  * @param value Value to match as one of the items in the list of content descriptions.
161  * @param substring Whether to use substring matching.
162  * @param ignoreCase Whether case should be ignored.
163  * @see SemanticsProperties.ContentDescription
164  */
165 fun hasContentDescription(
166     value: String,
167     substring: Boolean = false,
168     ignoreCase: Boolean = false
169 ): SemanticsMatcher {
170     return if (substring) {
171         SemanticsMatcher(
172             "${SemanticsProperties.ContentDescription.name} contains '$value' " +
173                 "(ignoreCase: $ignoreCase)"
174         ) {
175             it.config.getOrNull(SemanticsProperties.ContentDescription)?.any { item ->
176                 item.contains(value, ignoreCase)
177             } ?: false
178         }
179     } else {
180         SemanticsMatcher(
181             "${SemanticsProperties.ContentDescription.name} = '$value' (ignoreCase: $ignoreCase)"
182         ) {
183             it.config.getOrNull(SemanticsProperties.ContentDescription)?.any { item ->
184                 item.equals(value, ignoreCase)
185             } ?: false
186         }
187     }
188 }
189 
190 /**
191  * Returns whether the node's content description contains exactly the given [values] and nothing
192  * else.
193  *
194  * Note that in merged semantics tree there can be a list of content descriptions that got merged
195  * from the child nodes. Typically an accessibility tooling will decide based on its heuristics
196  * which ones to announce.
197  *
198  * @param values List of values to match (the order does not matter)
199  * @see SemanticsProperties.ContentDescription
200  */
hasContentDescriptionExactlynull201 fun hasContentDescriptionExactly(vararg values: String): SemanticsMatcher {
202     val expected = values.toList()
203     return SemanticsMatcher(
204         "${SemanticsProperties.ContentDescription.name} = " + "[${values.joinToString(",")}]"
205     ) { node ->
206         node.config.getOrNull(SemanticsProperties.ContentDescription)?.let { given ->
207             given.size == expected.size &&
208                 given.containsAll(expected) &&
209                 expected.containsAll(given)
210         } ?: values.isEmpty()
211     }
212 }
213 
214 /**
215  * Returns whether the node's text contains the given [text].
216  *
217  * This will also search in [SemanticsProperties.EditableText] and [SemanticsProperties.InputText].
218  *
219  * Note that in merged semantics tree there can be a list of text items that got merged from the
220  * child nodes. Typically an accessibility tooling will decide based on its heuristics which ones to
221  * use.
222  *
223  * @param text Value to match as one of the items in the list of text values.
224  * @param substring Whether to use substring matching.
225  * @param ignoreCase Whether case should be ignored.
226  * @see SemanticsProperties.Text
227  * @see SemanticsProperties.EditableText
228  */
hasTextnull229 fun hasText(
230     text: String,
231     substring: Boolean = false,
232     ignoreCase: Boolean = false
233 ): SemanticsMatcher {
234     val propertyName = "${Text.name} + ${InputText.name} + ${EditableText.name}"
235     return if (substring) {
236         SemanticsMatcher("$propertyName contains '$text' (ignoreCase: $ignoreCase) as substring") {
237             val isInInputTextValue =
238                 it.config.getOrNull(InputText)?.text?.contains(text, ignoreCase) ?: false
239             val isInEditableTextValue =
240                 it.config.getOrNull(EditableText)?.text?.contains(text, ignoreCase) ?: false
241             val isInTextValue =
242                 it.config.getOrNull(Text)?.any { item -> item.text.contains(text, ignoreCase) }
243                     ?: false
244             isInInputTextValue || isInEditableTextValue || isInTextValue
245         }
246     } else {
247         SemanticsMatcher("$propertyName contains '$text' (ignoreCase: $ignoreCase)") {
248             val isInInputTextValue =
249                 it.config.getOrNull(InputText)?.text?.equals(text, ignoreCase) ?: false
250             val isInEditableTextValue =
251                 it.config.getOrNull(EditableText)?.text?.equals(text, ignoreCase) ?: false
252             val isInTextValue =
253                 it.config.getOrNull(Text)?.any { item -> item.text.equals(text, ignoreCase) }
254                     ?: false
255             isInInputTextValue || isInEditableTextValue || isInTextValue
256         }
257     }
258 }
259 
260 /**
261  * Returns whether the node's text contains exactly the given [textValues] and nothing else.
262  *
263  * This will also search in [SemanticsProperties.EditableText] by default.
264  *
265  * Note that in merged semantics tree there can be a list of text items that got merged from the
266  * child nodes. Typically an accessibility tooling will decide based on its heuristics which ones to
267  * use.
268  *
269  * @param textValues List of values to match (the order does not matter)
270  * @param includeEditableText Whether to also assert against the editable text
271  * @see SemanticsProperties.Text
272  * @see SemanticsProperties.EditableText
273  */
hasTextExactlynull274 fun hasTextExactly(
275     vararg textValues: String,
276     includeEditableText: Boolean = true
277 ): SemanticsMatcher {
278     val expected = textValues.toList()
279     val propertyName =
280         if (includeEditableText) {
281             "${Text.name} + ${EditableText.name}"
282         } else {
283             Text.name
284         }
285     return SemanticsMatcher("$propertyName = [${textValues.joinToString(",")}]") { node ->
286         val actual = mutableListOf<String>()
287         if (includeEditableText) {
288             node.config.getOrNull(EditableText)?.let { actual.add(it.text) }
289         }
290         node.config.getOrNull(Text)?.let { actual.addAll(it.map { anStr -> anStr.text }) }
291         actual.containsAll(expected) && expected.containsAll(actual)
292     }
293 }
294 
295 /**
296  * Returns whether the node's value matches exactly to the given accessibility value.
297  *
298  * @param value Value to match.
299  * @see SemanticsProperties.StateDescription
300  */
hasStateDescriptionnull301 fun hasStateDescription(value: String): SemanticsMatcher =
302     SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, value)
303 
304 /**
305  * Returns whether the node is marked as an accessibility header.
306  *
307  * @see SemanticsProperties.Heading
308  */
309 fun isHeading(): SemanticsMatcher = hasKey(SemanticsProperties.Heading)
310 
311 /**
312  * Returns whether the node's range info matches exactly to the given accessibility range info.
313  *
314  * @param rangeInfo range info to match.
315  * @see SemanticsProperties.ProgressBarRangeInfo
316  */
317 fun hasProgressBarRangeInfo(rangeInfo: ProgressBarRangeInfo): SemanticsMatcher =
318     SemanticsMatcher.expectValue(SemanticsProperties.ProgressBarRangeInfo, rangeInfo)
319 
320 /**
321  * Returns whether the node is annotated by the given test tag.
322  *
323  * @param testTag Value to match.
324  * @see SemanticsProperties.TestTag
325  */
326 fun hasTestTag(testTag: String): SemanticsMatcher =
327     SemanticsMatcher.expectValue(SemanticsProperties.TestTag, testTag)
328 
329 /**
330  * Returns whether the node is a dialog.
331  *
332  * This only checks if the node itself is a dialog, not if it is _part of_ a dialog. Use
333  * `hasAnyAncestorThat(isDialog())` for that.
334  *
335  * @see SemanticsProperties.IsDialog
336  */
337 fun isDialog(): SemanticsMatcher = hasKey(SemanticsProperties.IsDialog)
338 
339 /**
340  * Returns whether the node is a popup.
341  *
342  * This only checks if the node itself is a popup, not if it is _part of_ a popup. Use
343  * `hasAnyAncestorThat(isPopup())` for that.
344  *
345  * @see SemanticsProperties.IsPopup
346  */
347 fun isPopup(): SemanticsMatcher = hasKey(SemanticsProperties.IsPopup)
348 
349 /**
350  * Returns whether the node defines the given IME action.
351  *
352  * @param actionType the action to match.
353  */
354 fun hasImeAction(actionType: ImeAction) =
355     SemanticsMatcher.expectValue(SemanticsProperties.ImeAction, actionType)
356 
357 /**
358  * Returns whether the node defines a semantics action to set text on it.
359  *
360  * This can be used to, for instance, filter out text fields.
361  *
362  * @see SemanticsActions.SetText
363  */
364 fun hasSetTextAction() = hasKey(SemanticsActions.SetText)
365 
366 /**
367  * Returns whether the node defines a semantics action to insert text on it.
368  *
369  * This can be used to, for instance, filter out text fields.
370  *
371  * @see SemanticsActions.InsertTextAtCursor
372  */
373 fun hasInsertTextAtCursorAction() = hasKey(SemanticsActions.InsertTextAtCursor)
374 
375 /**
376  * Returns whether the node defines a semantics action to perform the
377  * [IME action][SemanticsProperties.ImeAction] on it.
378  *
379  * @see SemanticsActions.OnImeAction
380  */
381 fun hasPerformImeAction() = hasKey(SemanticsActions.OnImeAction)
382 
383 /**
384  * Returns whether the node defines a semantics action to request focus.
385  *
386  * @see SemanticsActions.RequestFocus
387  */
388 fun hasRequestFocusAction() = hasKey(SemanticsActions.RequestFocus)
389 
390 /**
391  * Returns whether the node defines the ability to scroll to an item index.
392  *
393  * Note that not all scrollable containers have item indices. For example, a
394  * [scrollable][androidx.compose.foundation.gestures.scrollable] doesn't have items with an index,
395  * while [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] does.
396  */
397 fun hasScrollToIndexAction() = hasKey(SemanticsActions.ScrollToIndex)
398 
399 /**
400  * Returns whether the node defines the ability to scroll to an item identified by a key, such as
401  * [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] or
402  * [LazyRow][androidx.compose.foundation.lazy.LazyRow].
403  */
404 fun hasScrollToKeyAction() =
405     hasKey(SemanticsActions.ScrollToIndex).and(hasKey(SemanticsProperties.IndexForKey))
406 
407 /** Returns whether the node defines the ability to scroll to content identified by a matcher. */
408 fun hasScrollToNodeAction() =
409     hasKey(SemanticsActions.ScrollToIndex)
410         .and(hasKey(SemanticsActions.ScrollBy))
411         .and(
412             hasKey(SemanticsProperties.HorizontalScrollAxisRange)
413                 .or(hasKey(SemanticsProperties.VerticalScrollAxisRange))
414         )
415 
416 /**
417  * Returns whether the node is editable.
418  *
419  * @see SemanticsProperties.IsEditable
420  */
421 fun isEditable() = SemanticsMatcher.expectValue(SemanticsProperties.IsEditable, true)
422 
423 /**
424  * Return whether the node is the root semantics node.
425  *
426  * There is always one root in every node tree, added implicitly by Compose.
427  */
428 fun isRoot() = SemanticsMatcher("isRoot") { it.isRoot }
429 
430 /**
431  * Returns whether the node's parent satisfies the given matcher.
432  *
433  * Returns false if no parent exists.
434  */
hasParentnull435 fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher {
436     // TODO(b/150292800): If this is used in assert we should print the parent's node semantics
437     //  in the error message or say that no parent was found.
438     return SemanticsMatcher("hasParentThat(${matcher.description})") {
439         it.parent?.run { matcher.matches(this) } ?: false
440     }
441 }
442 
443 /** Returns whether the node has at least one child that satisfies the given matcher. */
hasAnyChildnull444 fun hasAnyChild(matcher: SemanticsMatcher): SemanticsMatcher {
445     // TODO(b/150292800): If this is used in assert we should print the children nodes semantics
446     //  in the error message or say that no children were found.
447     return SemanticsMatcher("hasAnyChildThat(${matcher.description})") {
448         matcher.matchesAny(it.children)
449     }
450 }
451 
452 /**
453  * Returns whether the node has at least one sibling that satisfies the given matcher.
454  *
455  * Sibling is defined as a any other node that shares the same parent.
456  */
hasAnySiblingnull457 fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher {
458     // TODO(b/150292800): If this is used in assert we should print the sibling nodes semantics
459     //  in the error message or say that no siblings were found.
460     return SemanticsMatcher("hasAnySiblingThat(${matcher.description})") {
461         val node = it
462         it.parent?.run { matcher.matchesAny(this.children.filter { child -> child.id != node.id }) }
463             ?: false
464     }
465 }
466 
467 /**
468  * Returns whether the node has at least one ancestor that satisfies the given matcher.
469  *
470  * Example: For the following tree
471  *
472  * ```
473  * |-X
474  * |-A
475  *   |-B
476  *     |-C1
477  *     |-C2
478  * ```
479  *
480  * In case of C1, we would check the matcher against A and B
481  */
hasAnyAncestornull482 fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher {
483     // TODO(b/150292800): If this is used in assert we should print the ancestor nodes semantics
484     //  in the error message or say that no ancestors were found.
485     return SemanticsMatcher("hasAnyAncestorThat(${matcher.description})") {
486         matcher.matchesAny(it.ancestors)
487     }
488 }
489 
490 /**
491  * Returns whether the node has at least one descendant that satisfies the given matcher.
492  *
493  * Example: For the following tree
494  *
495  * ```
496  * |-X
497  * |-A
498  *   |-B
499  *     |-C1
500  *     |-C2
501  * ```
502  *
503  * In case of A, we would check the matcher against B,C1 and C2
504  */
hasAnyDescendantnull505 fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher {
506     // TODO(b/150292800): If this is used in assert we could consider printing the whole subtree but
507     //  it might be too much to show. But we could at least warn if there were no ancestors found.
508     fun checkIfSubtreeMatches(matcher: SemanticsMatcher, node: SemanticsNode): Boolean {
509         if (matcher.matchesAny(node.children)) {
510             return true
511         }
512 
513         return node.children.fastAny { checkIfSubtreeMatches(matcher, it) }
514     }
515 
516     return SemanticsMatcher("hasAnyDescendantThat(${matcher.description})") {
517         checkIfSubtreeMatches(matcher, it)
518     }
519 }
520 
521 internal val SemanticsNode.ancestors: Iterable<SemanticsNode>
522     get() =
523         object : Iterable<SemanticsNode> {
iteratornull524             override fun iterator(): Iterator<SemanticsNode> {
525                 return object : Iterator<SemanticsNode> {
526                     @JsName("nextVar") var next = parent
527 
528                     override fun hasNext(): Boolean {
529                         return next != null
530                     }
531 
532                     override fun next(): SemanticsNode {
533                         return next!!.also { next = it.parent }
534                     }
535                 }
536             }
537         }
538 
hasKeynull539 private fun hasKey(key: SemanticsPropertyKey<*>): SemanticsMatcher =
540     SemanticsMatcher.keyIsDefined(key)
541