[go: nahoru, domu]

blob: 4ab61cd275879b520dcf3aa2198d3950f9e81260 [file] [log] [blame]
/*
* Copyright 2019 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.ui.test
import androidx.test.espresso.matcher.ViewMatchers
import androidx.ui.core.AndroidOwner
import androidx.ui.core.ExperimentalLayoutNodeApi
import androidx.ui.core.LayoutNode
import androidx.ui.core.findClosestParentNode
import androidx.ui.core.semantics.SemanticsNode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.ui.semantics.AccessibilityRangeInfo
import androidx.ui.semantics.SemanticsProperties
import androidx.ui.unit.height
import androidx.ui.unit.toRect
import androidx.ui.unit.width
/**
* Asserts that the current semantics node has hidden property set to true.
*
* Note that this does not verify parents of the node. For stronger guarantees of visibility
* see [assertIsNotDisplayed]. If you want to assert that the node is not even in the hierarchy
* use [SemanticsNodeInteraction.assertDoesNotExist].
*
* Throws [AssertionError] if the node is not hidden.
*/
fun SemanticsNodeInteraction.assertIsHidden(): SemanticsNodeInteraction = assert(isHidden())
/**
* Asserts that the current semantics node has hidden property set to false.
*
* Note that this does not verify parents of the node. For stronger guarantees of visibility
* see [assertIsDisplayed]. If you only want to assert that the node is in the hierarchy use
* [SemanticsNodeInteraction.assertExists]
*
* Throws [AssertionError] if the node is hidden.
*/
fun SemanticsNodeInteraction.assertIsNotHidden(): SemanticsNodeInteraction = assert(isNotHidden())
/**
* Asserts that the current semantics node is displayed on screen.
*
* Throws [AssertionError] if the node is not displayed.
*/
fun SemanticsNodeInteraction.assertIsDisplayed(): SemanticsNodeInteraction {
// TODO(b/143607231): check semantics hidden property
// TODO(b/143608742): check the correct AndroidCraneView is visible
if (!checkIsDisplayed()) {
// TODO(b/133217292)
throw AssertionError("Assert failed: The component is not displayed!")
}
return this
}
/**
* Asserts that the current semantics node is not displayed on screen.
*
* Throws [AssertionError] if the node is displayed.
*/
fun SemanticsNodeInteraction.assertIsNotDisplayed(): SemanticsNodeInteraction {
// TODO(b/143607231): check semantics hidden property
// TODO(b/143608742): check no AndroidCraneView contains the given component
if (checkIsDisplayed()) {
// TODO(b/133217292)
throw AssertionError("Assert failed: The component is displayed!")
}
return this
}
/**
* Asserts that the current semantics node is enabled.
*
* Throws [AssertionError] if the node is not enabled or does not define the property at all.
*/
fun SemanticsNodeInteraction.assertIsEnabled(): SemanticsNodeInteraction = assert(isEnabled())
/**
* Asserts that the current semantics node is not enabled.
*
* Throws [AssertionError] if the node is enabled or does not defined the property at all.
*/
fun SemanticsNodeInteraction.assertIsNotEnabled(): SemanticsNodeInteraction = assert(isNotEnabled())
/**
* Asserts that the current semantics node is checked.
*
* Throws [AssertionError] if the node is not unchecked, indeterminate, or not toggleable.
*/
fun SemanticsNodeInteraction.assertIsOn(): SemanticsNodeInteraction = assert(isOn())
/**
* Asserts that the current semantics node is unchecked.
*
* Throws [AssertionError] if the node is checked, indeterminate, or not toggleable.
*/
fun SemanticsNodeInteraction.assertIsOff(): SemanticsNodeInteraction = assert(isOff())
/**
* Asserts that the current semantics node is selected.
*
* Throws [AssertionError] if the node is unselected or not selectable.
*/
fun SemanticsNodeInteraction.assertIsSelected(): SemanticsNodeInteraction = assert(isSelected())
/**
* Asserts that the current semantics node is not selected.
*
* Throws [AssertionError] if the node is selected or not selectable.
*/
fun SemanticsNodeInteraction.assertIsNotSelected(): SemanticsNodeInteraction =
assert(isNotSelected())
/**
* Asserts that the current semantics node is toggleable.
*
* Throws [AssertionError] if the node is not toggleable.
*/
fun SemanticsNodeInteraction.assertIsToggleable(): SemanticsNodeInteraction =
assert(isToggleable())
/**
* Asserts that the current semantics node is selectable.
*
* Throws [AssertionError] if the node is not selectable.
*/
fun SemanticsNodeInteraction.assertIsSelectable(): SemanticsNodeInteraction =
assert(isSelectable())
/**
* Asserts the semantics node is in a mutually exclusive group. This is used by radio groups to
* assert only one is selected at a given time.
*/
fun SemanticsNodeInteraction.assertIsInMutuallyExclusiveGroup(): SemanticsNodeInteraction =
assert(isInMutuallyExclusiveGroup())
/**
* Asserts the node's label equals the given String.
* For further details please check [SemanticsProperties.AccessibilityLabel].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertLabelEquals(value: String): SemanticsNodeInteraction =
assert(hasLabel(value))
/**
* Asserts the node's text equals the given String.
* For further details please check [SemanticsProperties.Text].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertTextEquals(value: String): SemanticsNodeInteraction =
assert(hasText(value))
/**
* Asserts the node's value equals the given value.
*
* For further details please check [SemanticsProperties.AccessibilityValue].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertValueEquals(value: String): SemanticsNodeInteraction =
assert(hasValue(value))
/**
* Asserts the node's range info equals the given value.
*
* For further details please check [SemanticsProperties.AccessibilityRangeInfo].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertRangeInfoEquals(value: AccessibilityRangeInfo):
SemanticsNodeInteraction =
assert(hasRangeInfo(value))
/**
* Asserts that the current semantics node has a click action.
*
* Throws [AssertionError] if the node is doesn't have a click action.
*/
fun SemanticsNodeInteraction.assertHasClickAction(): SemanticsNodeInteraction =
assert(hasClickAction())
/**
* Asserts that the current semantics node has doesn't have a click action.
*
* Throws [AssertionError] if the node has a click action.
*/
fun SemanticsNodeInteraction.assertHasNoClickAction(): SemanticsNodeInteraction =
assert(hasNoClickAction())
/**
* Asserts that the provided [matcher] is satisfied for this node.
*
* @param matcher Matcher to verify.
* @param messagePrefixOnError Prefix to be put in front of an error that gets thrown in case this
* assert fails. This can be helpful in situations where this assert fails as part of a bigger
* operation that used this assert as a precondition check.
*
* @throws AssertionError if the matcher does not match or the node can no longer be found.
*/
fun SemanticsNodeInteraction.assert(
matcher: SemanticsMatcher,
messagePrefixOnError: (() -> String)? = null
): SemanticsNodeInteraction {
var errorMessageOnFail = "Failed to assert the following: (${matcher.description})"
if (messagePrefixOnError != null) {
errorMessageOnFail = messagePrefixOnError() + "\n" + errorMessageOnFail
}
val node = fetchSemanticsNode(errorMessageOnFail)
if (!matcher.matches(node)) {
throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, selector, node))
}
return this
}
/**
* Asserts that this collection of nodes is equal to the given [expectedSize].
*
* Provides a detailed error message on failure.
*
* @throws AssertionError if the size is not equal to [expectedSize]
*/
fun SemanticsNodeInteractionCollection.assertCountEquals(
expectedSize: Int
): SemanticsNodeInteractionCollection {
val errorOnFail = "Failed to assert count of nodes."
val matchedNodes = fetchSemanticsNodes(errorOnFail)
if (matchedNodes.size != expectedSize) {
throw AssertionError(buildErrorMessageForCountMismatch(
errorMessage = errorOnFail,
selector = selector,
foundNodes = matchedNodes,
expectedCount = expectedSize))
}
return this
}
/**
* Asserts that this collection contains at least one element that satisfies the given [matcher].
*
* @param matcher Matcher that has to be satisfied by at least one of the nodes in the collection.
*
* @throws AssertionError if not at least one matching node was node.
*/
fun SemanticsNodeInteractionCollection.assertAny(
matcher: SemanticsMatcher
): SemanticsNodeInteractionCollection {
val errorOnFail = "Failed to assertAny(${matcher.description})"
val nodes = fetchSemanticsNodes(errorOnFail)
if (nodes.isEmpty()) {
throw AssertionError(buildErrorMessageForAtLeastOneNodeExpected(errorOnFail, selector))
}
if (!matcher.matchesAny(nodes)) {
throw AssertionError(buildErrorMessageForAssertAnyFail(selector, nodes, matcher))
}
return this
}
/**
* Asserts that all the nodes in this collection satisfy the given [matcher].
*
* This passes also for empty collections.
*
* @param matcher Matcher that has to be satisfied by all the nodes in the collection.
*
* @throws AssertionError if the collection contains at least one element that does not satisfy
* the given matcher.
*/
fun SemanticsNodeInteractionCollection.assertAll(
matcher: SemanticsMatcher
): SemanticsNodeInteractionCollection {
val errorOnFail = "Failed to assertAll(${matcher.description})"
val nodes = fetchSemanticsNodes(errorOnFail)
val violations = mutableListOf<SemanticsNode>()
nodes.forEach {
if (!matcher.matches(it)) {
violations.add(it)
}
}
if (violations.isNotEmpty()) {
throw AssertionError(buildErrorMessageForAssertAllFail(selector, violations, matcher))
}
return this
}
@OptIn(ExperimentalLayoutNodeApi::class)
private fun SemanticsNodeInteraction.checkIsDisplayed(): Boolean {
// hierarchy check - check layout nodes are visible
val errorMessageOnFail = "Failed to perform isDisplayed check."
val node = fetchSemanticsNode(errorMessageOnFail)
fun isNotPlaced(node: LayoutNode): Boolean {
return !node.isPlaced
}
val layoutNode = node.componentNode
if (isNotPlaced(layoutNode) || layoutNode.findClosestParentNode(::isNotPlaced) != null) {
return false
}
(layoutNode.owner as? AndroidOwner)?.let {
if (!ViewMatchers.isDisplayed().matches(it.view)) {
return false
}
}
// check node doesn't clip unintentionally (e.g. row too small for content)
val globalRect = node.globalBounds
if (!node.isInScreenBounds()) {
return false
}
return (globalRect.width > 0f && globalRect.height > 0f)
}
@OptIn(ExperimentalLayoutNodeApi::class)
private fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
val composeView = (componentNode.owner as AndroidOwner).view
val rootLocationInWindow = intArrayOf(0, 0).let {
composeView.getLocationInWindow(it)
Offset(it[0].toFloat(), it[1].toFloat())
}
return boundsInRoot.toRect().shift(rootLocationInWindow)
}
@OptIn(ExperimentalLayoutNodeApi::class)
private fun SemanticsNode.isInScreenBounds(): Boolean {
val composeView = (componentNode.owner as AndroidOwner).view
// Window relative bounds of our node
val nodeBoundsInWindow = clippedNodeBoundsInWindow()
if (nodeBoundsInWindow.width == 0f || nodeBoundsInWindow.height == 0f) {
return false
}
// Window relative bounds of our compose root view that are visible on the screen
val globalRootRect = android.graphics.Rect()
if (!composeView.getGlobalVisibleRect(globalRootRect)) {
return false
}
return nodeBoundsInWindow.top >= globalRootRect.top &&
nodeBoundsInWindow.left >= globalRootRect.left &&
nodeBoundsInWindow.right <= globalRootRect.right &&
nodeBoundsInWindow.bottom <= globalRootRect.bottom
}