[go: nahoru, domu]

blob: 27a6fd33c4d3589c1777e82f19b8b3884cbbf49b [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 android.os.Handler
import android.os.Looper
import androidx.ui.core.findClosestParentNode
import androidx.ui.core.findLastLayoutChild
import androidx.ui.semantics.AccessibilityAction
import androidx.ui.semantics.SemanticsActions
import androidx.ui.semantics.SemanticsPropertyKey
import androidx.ui.unit.PxPosition
import androidx.ui.unit.px
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Performs a click action on the given component.
*/
fun SemanticsNodeInteraction.doClick(): SemanticsNodeInteraction {
// TODO(jellefresen): Replace with semantics action when semantics merging is done
// The problem we currently have is that the click action might be defined on a different
// semantics node than we're interacting with now, even though it is "semantically" the same.
// E.g., findByText(buttonText) finds the Text's semantics node, but the click action is
// defined on the wrapping Button's semantics node.
// Since in general the intended click action can be on a wrapping node or a child node, we
// can't just forward to the correct node, as we don't know if we should search up or down the
// tree.
return doGesture {
sendClick()
}
}
/**
* Scrolls to a component using SemanticsActions. It first identifies a parent component with a
* Semantics ScrollTo action, then it retrieves the location of the current element and computes
* the relative coordinates that will be used by the scroller.
*
* Throws [AssertionError] if there is no parent component with ScrollTo SemanticsAction, the
* current semantics component doesn't have a bounding rectangle set or if a layout node used to
* compute the relative coordinates to be fed to the ScrollTo action can't be found.
*/
fun SemanticsNodeInteraction.doScrollTo(): SemanticsNodeInteraction {
// find containing component with scroll action
val errorMessageOnFail = "Failed to perform doScrollTo."
val node = fetchSemanticsNode(errorMessageOnFail)
val scrollableSemanticsNode = node.findClosestParentNode {
hasScrollAction().condition(it)
}
?: throw AssertionError(
"Semantic Node has no parent layout with a Scroll SemanticsAction"
)
val globalPosition = node.globalPosition
val layoutNode = scrollableSemanticsNode.componentNode.findLastLayoutChild { true }
?: throw AssertionError(
"No Layout Node found!"
)
val position = layoutNode.coordinates.localToGlobal(PxPosition(0.px, 0.px))
runOnUiThread {
scrollableSemanticsNode.config[SemanticsActions.ScrollTo].action(
globalPosition.x - position.x,
globalPosition.y - position.y
)
}
return this
}
/**
* Executes the gestures specified in the given block.
*
* Example usage:
* findByTag("myWidget")
* .doGesture {
* sendSwipeUp()
* }
*/
fun SemanticsNodeInteraction.doGesture(
block: GestureScope.() -> Unit
): SemanticsNodeInteraction {
val scope = GestureScope(this)
scope.block()
return this
}
/**
* Provides support to call custom semantics actions on this node.
*
* This method is supposed to be used for actions with parameters.
*
* This will properly verify that the actions exists and provide clear error message in case it
* does not. It also handle synchronization and performing the action on the UI thread. This call
* is blocking until the action is performed
*
* @param key Key of the action to be performed.
* @param invocation Place where you call your action. In the argument is provided the underlying
* action from the given Semantics action.
*
* @throws AssertionError If the semantics action is not defined on this node.
*/
fun <T : Function<Unit>> SemanticsNodeInteraction.callSemanticsAction(
key: SemanticsPropertyKey<AccessibilityAction<T>>,
invocation: (T) -> Unit
) {
val node = fetchSemanticsNode("Failed to call ${key.name} action.")
if (key !in node.config) {
throw AssertionError(
buildGeneralErrorMessage(
"Failed to call ${key.name} action as it is not defined on the node.",
semanticsTreeInteraction.selector, node)
)
}
runOnUiThread {
invocation(node.config[key].action)
}
}
/**
* Provides support to call custom semantics actions on this node.
*
* This method is for calling actions that have no parameters.
*
* This will properly verify that the actions exists and provide clear error message in case it
* does not. It also handle synchronization and performing the action on the UI thread. This call
* is blocking until the action is performed
*
* @param key Key of the action to be performed.
*
* @throws AssertionError If the semantics action is not defined on this node.
*/
fun SemanticsNodeInteraction.callSemanticsAction(
key: SemanticsPropertyKey<AccessibilityAction<() -> Unit>>
) {
callSemanticsAction(key) { it.invoke() }
}
// TODO(pavlis): We could expose this to the developers to avoid their dependency on TestRules
// After that we can build runOnIdleCompose on top of this and move that out of ComposeTestRule
private fun runOnUiThread(action: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
action.invoke()
return
}
val latch = CountDownLatch(1)
val handler = Handler(Looper.getMainLooper())
handler.post(object : Runnable {
override fun run() {
action.invoke()
latch.countDown()
}
})
val success = latch.await(1_000, TimeUnit.MILLISECONDS)
if (!success) {
throw TimeoutException("Timeout has occurred when waiting for an action to be performed.")
}
}