| /* |
| * 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.ui.geometry.Offset |
| import androidx.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 unselected. |
| * |
| * Throws [AssertionError] if the node is selected or not selectable. |
| */ |
| fun SemanticsNodeInteraction.assertIsUnselected(): SemanticsNodeInteraction = |
| assert(isUnselected()) |
| |
| /** |
| * 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 |
| } |