commonMain.androidx.compose.ui.test.SemanticsSelector.kt Maven / Gradle / Ivy
/*
* Copyright 2020 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.compose.ui.test
import androidx.compose.ui.semantics.SemanticsNode
/**
* Projects the given set of nodes to a new set of nodes.
*
* @param description Description that is displayed to the developer in error outputs.
* @param requiresExactlyOneNode Whether this selector should expect to receive exactly 1 node.
* @param chainedInputSelector Optional selector to apply before this selector gets applied.
* @param selector The lambda that implements the projection.
*/
class SemanticsSelector(
val description: String,
private val requiresExactlyOneNode: Boolean,
private val chainedInputSelector: SemanticsSelector? = null,
private val selector: (Iterable) -> SelectionResult
) {
/**
* Maps the given list of nodes to a new list of nodes.
*
* @throws AssertionError if required prerequisites to perform the selection were not satisfied.
*/
fun map(nodes: Iterable, errorOnFail: String): SelectionResult {
val chainedResult = chainedInputSelector?.map(nodes, errorOnFail)
val inputNodes = chainedResult?.selectedNodes ?: nodes
if (requiresExactlyOneNode && inputNodes.count() != 1) {
throw AssertionError(
chainedResult?.customErrorOnNoMatch ?: buildErrorMessageForCountMismatch(
errorMessage = errorOnFail,
foundNodes = inputNodes.toList(),
expectedCount = 1,
selector = chainedInputSelector ?: this
)
)
}
return selector(inputNodes)
}
}
/**
* Creates a new [SemanticsSelector] based on the given [SemanticsMatcher].
*/
internal fun SemanticsSelector(matcher: SemanticsMatcher): SemanticsSelector {
return SemanticsSelector(
matcher.description,
requiresExactlyOneNode = false,
chainedInputSelector = null
) {
nodes ->
SelectionResult(nodes.filter { matcher.matches(it) })
}
}
/**
* Result of [SemanticsSelector] projection.
*
* @param selectedNodes The result nodes found.
* @param customErrorOnNoMatch If the projection failed to map nodes due to wrong input (e.g.
* selector expected only 1 node but got multiple) it will provide a custom error exactly explaining
* what selection was performed and what nodes it received.
*/
class SelectionResult(
val selectedNodes: List,
val customErrorOnNoMatch: String? = null
)
/**
* Chains the given selector to be performed after this one.
*
* The new selector will expect to receive exactly one node (otherwise will fail).
*/
internal fun SemanticsSelector.addSelectionFromSingleNode(
description: String,
selector: (SemanticsNode) -> List
): SemanticsSelector {
return SemanticsSelector(
"(${this.description}).$description",
requiresExactlyOneNode = true,
chainedInputSelector = this
) {
nodes ->
SelectionResult(selector(nodes.first()))
}
}
/**
* Chains a new selector that retrieves node from this selector at the given [index].
*/
internal fun SemanticsSelector.addIndexSelector(
index: Int
): SemanticsSelector {
return SemanticsSelector(
"(${this.description})[$index]",
requiresExactlyOneNode = false,
chainedInputSelector = this
) { nodes ->
val nodesList = nodes.toList()
if (index >= 0 && index < nodesList.size) {
SelectionResult(listOf(nodesList[index]))
} else {
val errorMessage = buildIndexErrorMessage(index, this, nodesList)
SelectionResult(emptyList(), errorMessage)
}
}
}
/**
* Chains a new selector that retrieves the last node returned from this selector.
*/
internal fun SemanticsSelector.addLastNodeSelector(): SemanticsSelector {
return SemanticsSelector(
"(${this.description}).last",
requiresExactlyOneNode = false,
chainedInputSelector = this
) { nodes ->
SelectionResult(nodes.toList().takeLast(1))
}
}
/**
* Chains a new selector that selects all the nodes matching the given [matcher] from the nodes
* returned by this selector.
*/
internal fun SemanticsSelector.addSelectorViaMatcher(
selectorName: String,
matcher: SemanticsMatcher
): SemanticsSelector {
return SemanticsSelector(
"(${this.description}).$selectorName(${matcher.description})",
requiresExactlyOneNode = false,
chainedInputSelector = this
) { nodes ->
SelectionResult(nodes.filter { matcher.matches(it) })
}
}