GlanceNodeSelector.kt
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.glance.testing
import androidx.annotation.RestrictTo
/**
* A chainable selector that allows specifying how to select nodes from a collection.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class GlanceNodeSelector<R>(
val description: String,
private val previousChainedSelector: GlanceNodeSelector<R>? = null,
private val selector: (Iterable<GlanceNode<R>>) -> SelectionResult<R>
) {
/**
* Returns nodes selected by previous chained selectors followed by the current selector.
*/
fun map(nodes: Iterable<GlanceNode<R>>): SelectionResult<R> {
val previousSelectionResult = previousChainedSelector?.map(nodes)
val inputNodes = previousSelectionResult?.selectedNodes ?: nodes
return selector(inputNodes)
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class SelectionResult<R>(
val selectedNodes: List<GlanceNode<R>>,
val errorMessageOnNoMatch: String? = null
)
/**
* Constructs an entry-point selector that selects nodes satisfying the matcher condition. Used at
* the entry points such as [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] where there is no previous chained selector.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <R> GlanceNodeMatcher<R>.matcherToSelector(): GlanceNodeSelector<R> {
return GlanceNodeSelector(
description = description,
previousChainedSelector = null
) { glanceNodes ->
SelectionResult(
selectedNodes = glanceNodes.filter { matches(it) }
)
}
}
/**
* Wraps the current selector with a chained selector that selects a node at a given index from the
* the result of current selection.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <R> GlanceNodeSelector<R>.addIndexedSelector(index: Int): GlanceNodeSelector<R> {
return GlanceNodeSelector(
description = "(${this.description})[$index]",
previousChainedSelector = this
) { nodes ->
val nodesList = nodes.toList()
val minimumExpectedCount = index + 1
if (index >= 0 && index < nodesList.size) {
SelectionResult(
selectedNodes = listOf(nodesList[index])
)
} else {
SelectionResult(
selectedNodes = emptyList(),
errorMessageOnNoMatch = buildErrorReasonForIndexOutOfMatchedNodeBounds(
description,
requestedIndex = minimumExpectedCount,
actualCount = nodesList.size
)
)
}
}
}
/**
* Wraps the current selector with a chained matcher-based selector that filters the list of nodes
* to return ones matched by the matcher.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <R> GlanceNodeSelector<R>.addMatcherSelector(
selectorName: String,
matcher: GlanceNodeMatcher<R>
): GlanceNodeSelector<R> {
return GlanceNodeSelector(
description = "(${this.description}).$selectorName(${matcher.description})",
previousChainedSelector = this
) { nodes ->
SelectionResult(
selectedNodes = nodes.filter { matcher.matches(it) }
)
}
}
/**
* Wraps the current selector with a chained matcher-based selector that ensures only one node is
* returned by current selector and selects children of that node.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <R> GlanceNodeSelector<R>.addChildrenSelector(): GlanceNodeSelector<R> {
return GlanceNodeSelector(
description = "($description).children()",
previousChainedSelector = this
) { nodes ->
if (nodes.count() != 1) {
SelectionResult(
selectedNodes = emptyList(),
errorMessageOnNoMatch = buildErrorReasonForCountMismatch(
matcherDescription = description,
expectedCount = 1,
actualCount = nodes.count()
)
)
} else {
SelectionResult(
selectedNodes = nodes.single().children()
)
}
}
}