1 /*
<lambda>null2  * Copyright 2020 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.SemanticsNode
20 
21 /**
22  * Projects the given set of nodes to a new set of nodes.
23  *
24  * @param description Description that is displayed to the developer in error outputs.
25  * @param requiresExactlyOneNode Whether this selector should expect to receive exactly 1 node.
26  * @param chainedInputSelector Optional selector to apply before this selector gets applied.
27  * @param selector The lambda that implements the projection.
28  */
29 class SemanticsSelector(
30     val description: String,
31     private val requiresExactlyOneNode: Boolean,
32     private val chainedInputSelector: SemanticsSelector? = null,
33     private val selector: (Iterable<SemanticsNode>) -> SelectionResult
34 ) {
35 
36     /**
37      * Maps the given list of nodes to a new list of nodes.
38      *
39      * @throws AssertionError if required prerequisites to perform the selection were not satisfied.
40      */
41     fun map(nodes: Iterable<SemanticsNode>, errorOnFail: String): SelectionResult {
42         val chainedResult = chainedInputSelector?.map(nodes, errorOnFail)
43         val inputNodes = chainedResult?.selectedNodes ?: nodes
44         if (requiresExactlyOneNode && inputNodes.count() != 1) {
45             throw AssertionError(
46                 chainedResult?.customErrorOnNoMatch
47                     ?: buildErrorMessageForCountMismatch(
48                         errorMessage = errorOnFail,
49                         foundNodes = inputNodes.toList(),
50                         expectedCount = 1,
51                         selector = chainedInputSelector ?: this
52                     )
53             )
54         }
55         return selector(inputNodes)
56     }
57 }
58 
59 /** Creates a new [SemanticsSelector] based on the given [SemanticsMatcher]. */
SemanticsSelectornull60 internal fun SemanticsSelector(matcher: SemanticsMatcher): SemanticsSelector {
61     return SemanticsSelector(
62         matcher.description,
63         requiresExactlyOneNode = false,
64         chainedInputSelector = null
65     ) { nodes ->
66         SelectionResult(nodes.filter { matcher.matches(it) })
67     }
68 }
69 
70 /**
71  * Result of [SemanticsSelector] projection.
72  *
73  * @param selectedNodes The result nodes found.
74  * @param customErrorOnNoMatch If the projection failed to map nodes due to wrong input (e.g.
75  *   selector expected only 1 node but got multiple) it will provide a custom error exactly
76  *   explaining what selection was performed and what nodes it received.
77  */
78 class SelectionResult(
79     val selectedNodes: List<SemanticsNode>,
80     val customErrorOnNoMatch: String? = null
81 )
82 
83 /**
84  * Chains the given selector to be performed after this one.
85  *
86  * The new selector will expect to receive exactly one node (otherwise will fail).
87  */
addSelectionFromSingleNodenull88 internal fun SemanticsSelector.addSelectionFromSingleNode(
89     description: String,
90     selector: (SemanticsNode) -> List<SemanticsNode>
91 ): SemanticsSelector {
92     return SemanticsSelector(
93         "(${this.description}).$description",
94         requiresExactlyOneNode = true,
95         chainedInputSelector = this
96     ) { nodes ->
97         SelectionResult(selector(nodes.first()))
98     }
99 }
100 
101 /** Chains a new selector that retrieves node from this selector at the given [index]. */
addIndexSelectornull102 internal fun SemanticsSelector.addIndexSelector(index: Int): SemanticsSelector {
103     return SemanticsSelector(
104         "(${this.description})[$index]",
105         requiresExactlyOneNode = false,
106         chainedInputSelector = this
107     ) { nodes ->
108         val nodesList = nodes.toList()
109         if (index >= 0 && index < nodesList.size) {
110             SelectionResult(listOf(nodesList[index]))
111         } else {
112             val errorMessage = buildIndexErrorMessage(index, this, nodesList)
113             SelectionResult(emptyList(), errorMessage)
114         }
115     }
116 }
117 
118 /** Chains a new selector that retrieves the last node returned from this selector. */
addLastNodeSelectornull119 internal fun SemanticsSelector.addLastNodeSelector(): SemanticsSelector {
120     return SemanticsSelector(
121         "(${this.description}).last",
122         requiresExactlyOneNode = false,
123         chainedInputSelector = this
124     ) { nodes ->
125         SelectionResult(nodes.toList().takeLast(1))
126     }
127 }
128 
129 /**
130  * Chains a new selector that selects all the nodes matching the given [matcher] from the nodes
131  * returned by this selector.
132  */
addSelectorViaMatchernull133 internal fun SemanticsSelector.addSelectorViaMatcher(
134     selectorName: String,
135     matcher: SemanticsMatcher
136 ): SemanticsSelector {
137     return SemanticsSelector(
138         "(${this.description}).$selectorName(${matcher.description})",
139         requiresExactlyOneNode = false,
140         chainedInputSelector = this
141     ) { nodes ->
142         SelectionResult(nodes.filter { matcher.matches(it) })
143     }
144 }
145