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