1 /*
<lambda>null2  * Copyright 2024 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.xr.compose.testing
18 
19 import androidx.xr.compose.subspace.node.SubspaceSemanticsInfo
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 internal class SubspaceSemanticsSelector(
30     internal val description: String,
31     private val requiresExactlyOneNode: Boolean,
32     private val chainedInputSelector: SubspaceSemanticsSelector? = null,
33     private val selector: (Iterable<SubspaceSemanticsInfo>) -> SubspaceSelectionResult,
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     internal fun map(
42         nodes: Iterable<SubspaceSemanticsInfo>,
43         errorOnFail: String,
44     ): SubspaceSelectionResult {
45         val chainedResult = chainedInputSelector?.map(nodes, errorOnFail)
46         val inputNodes = chainedResult?.selectedNodes ?: nodes
47         if (requiresExactlyOneNode && inputNodes.count() != 1) {
48             throw AssertionError(
49                 chainedResult?.customErrorOnNoMatch
50                     ?: "Required exactly one node but found ${inputNodes.count()} nodes."
51             )
52         }
53         return selector(inputNodes)
54     }
55 }
56 
57 /** Creates a new [SubspaceSemanticsSelector] based on the given [SubspaceSemanticsMatcher]. */
SubspaceSemanticsSelectornull58 internal fun SubspaceSemanticsSelector(
59     matcher: SubspaceSemanticsMatcher
60 ): SubspaceSemanticsSelector {
61     return SubspaceSemanticsSelector(
62         matcher.description,
63         requiresExactlyOneNode = false,
64         chainedInputSelector = null,
65     ) { nodes ->
66         SubspaceSelectionResult(nodes.filter { matcher.matches(it) })
67     }
68 }
69 
70 /**
71  * Result of [SubspaceSemanticsSelector] 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 internal class SubspaceSelectionResult(
79     internal val selectedNodes: List<SubspaceSemanticsInfo>,
80     internal 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 SubspaceSemanticsSelector.addSelectionFromSingleNode(
89     description: String,
90     selector: (SubspaceSemanticsInfo) -> List<SubspaceSemanticsInfo>,
91 ): SubspaceSemanticsSelector {
92     return SubspaceSemanticsSelector(
93         "(${this.description}).$description",
94         requiresExactlyOneNode = true,
95         chainedInputSelector = this,
96     ) { nodes ->
97         SubspaceSelectionResult(selector(nodes.first()))
98     }
99 }
100 
101 /** Chains a new selector that retrieves node from this selector at the given [index]. */
addIndexSelectornull102 internal fun SubspaceSemanticsSelector.addIndexSelector(index: Int): SubspaceSemanticsSelector {
103     return SubspaceSemanticsSelector(
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             SubspaceSelectionResult(listOf(nodesList[index]))
111         } else {
112             val errorMessage = "Index out of bounds: $index"
113             SubspaceSelectionResult(emptyList(), errorMessage)
114         }
115     }
116 }
117 
118 /** Chains a new selector that retrieves the last node returned from this selector. */
addLastNodeSelectornull119 internal fun SubspaceSemanticsSelector.addLastNodeSelector(): SubspaceSemanticsSelector {
120     return SubspaceSemanticsSelector(
121         "(${this.description}).last",
122         requiresExactlyOneNode = false,
123         chainedInputSelector = this,
124     ) { nodes ->
125         SubspaceSelectionResult(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 SubspaceSemanticsSelector.addSelectorViaMatcher(
134     selectorName: String,
135     matcher: SubspaceSemanticsMatcher,
136 ): SubspaceSemanticsSelector {
137     return SubspaceSemanticsSelector(
138         "(${this.description}).$selectorName(${matcher.description})",
139         requiresExactlyOneNode = false,
140         chainedInputSelector = this,
141     ) { nodes ->
142         SubspaceSelectionResult(nodes.filter { matcher.matches(it) })
143     }
144 }
145