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