Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.compose.ui.test
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties.HorizontalScrollAxisRange
import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import kotlin.jvm.JvmName
import kotlin.math.abs
import kotlin.math.sign
internal expect fun SemanticsNodeInteraction.performClickImpl(): SemanticsNodeInteraction
/**
* Performs a click action on the element represented by the given semantics node. Depending on
* the platform this may be implemented by a touch click (tap), a mouse click, or another more
* appropriate method for that platform.
*
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*/
fun SemanticsNodeInteraction.performClick(): SemanticsNodeInteraction {
return performClickImpl()
}
/**
* Scrolls the closest enclosing scroll parent by the smallest amount such that this node is fully
* visible in its viewport. If this node is larger than the viewport, scrolls the scroll parent
* by the smallest amount such that this node fills the entire viewport. A scroll parent is a
* parent node that has the semantics action [SemanticsActions.ScrollBy] (usually implemented by
* defining [scrollBy][androidx.compose.ui.semantics.scrollBy]).
*
* This action should be performed on the [node][SemanticsNodeInteraction] that is part of the
* scrollable content, not on the scrollable container.
*
* Throws an [AssertionError] if there is no scroll parent.
*
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*/
fun SemanticsNodeInteraction.performScrollTo(): SemanticsNodeInteraction {
@OptIn(InternalTestApi::class)
fetchSemanticsNode("Action performScrollTo() failed.").scrollToNode(testContext.testOwner)
return this
}
/**
* Implementation of [performScrollTo]
*/
@OptIn(InternalTestApi::class)
private fun SemanticsNode.scrollToNode(testOwner: TestOwner) {
val scrollableNode = findClosestParentNode {
hasScrollAction().matches(it)
} ?: throw AssertionError("Semantic Node has no parent layout with a Scroll SemanticsAction")
// Figure out the (clipped) bounds of the viewPort in its direct parent's content area, in
// root coordinates. We only want the clipping from the direct parent on the scrollable, not
// from any other ancestors.
val viewportInParent = scrollableNode.layoutInfo.coordinates.boundsInParent()
val parentInRoot = scrollableNode.layoutInfo.coordinates.parentLayoutCoordinates
?.positionInRoot() ?: Offset.Zero
val viewport = viewportInParent.translate(parentInRoot)
val target = Rect(positionInRoot, size.toSize())
// Given the desired scroll value to align either side of the target with the
// viewport, what delta should we go with?
// If we need to scroll in opposite directions for both sides, don't scroll at all.
// Otherwise, take the delta that scrolls the least amount.
fun scrollDelta(a: Float, b: Float): Float =
if (sign(a) == sign(b)) if (abs(a) < abs(b)) a else b else 0f
// Get the desired delta X
var dx = scrollDelta(target.left - viewport.left, target.right - viewport.right)
// And adjust for reversing properties
if (scrollableNode.isReversedHorizontally) dx = -dx
if (scrollableNode.isRtl) dx = -dx
// Get the desired delta Y
var dy = scrollDelta(target.top - viewport.top, target.bottom - viewport.bottom)
// And adjust for reversing properties
if (scrollableNode.isReversedVertically) dy = -dy
testOwner.runOnUiThread {
scrollableNode.config[ScrollBy].action?.invoke(dx, dy)
}
}
/**
* Scrolls a scrollable container with items to the item with the given [index].
*
* Note that not all scrollable containers have item indices. For example, a
* [scrollable][androidx.compose.foundation.gestures.scrollable] doesn't have items with an
* index, while [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] does.
*
* This action should be performed on a [node][SemanticsNodeInteraction] that is a scrollable
* container, not on a node that is part of the content of that container.
*
* Throws an [AssertionError] if the node doesn't have [ScrollToIndex] defined.
*
* @param index The index of the item to scroll to
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @see hasScrollToIndexAction
*/
fun SemanticsNodeInteraction.performScrollToIndex(index: Int): SemanticsNodeInteraction {
fetchSemanticsNode("Failed: performScrollToIndex($index)").scrollToIndex(index, this)
return this
}
/**
* Implementation of [performScrollToIndex]
*/
private fun SemanticsNode.scrollToIndex(index: Int, nodeInteraction: SemanticsNodeInteraction) {
nodeInteraction.requireSemantics(this, ScrollToIndex) {
"Failed to scroll to index $index"
}
@OptIn(InternalTestApi::class)
nodeInteraction.testContext.testOwner.runOnUiThread {
config[ScrollToIndex].action!!.invoke(index)
}
}
/**
* Scrolls a scrollable container with keyed items to the item with the given [key], such as
* [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] or
* [LazyRow][androidx.compose.foundation.lazy.LazyRow].
*
* This action should be performed on a [node][SemanticsNodeInteraction] that is a scrollable
* container, not on a node that is part of the content of that container.
*
* Throws an [AssertionError] if the node doesn't have [IndexForKey] or [ScrollToIndex] defined.
*
* @param key The key of the item to scroll to
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @see hasScrollToKeyAction
*/
fun SemanticsNodeInteraction.performScrollToKey(key: Any): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed: performScrollToKey(\"$key\")")
requireSemantics(node, IndexForKey, ScrollToIndex) {
"Failed to scroll to the item identified by \"$key\""
}
val index = node.config[IndexForKey].invoke(key)
require(index >= 0) {
"Failed to scroll to the item identified by \"$key\", couldn't find the key."
}
@OptIn(InternalTestApi::class)
testContext.testOwner.runOnUiThread {
node.config[ScrollToIndex].action!!.invoke(index)
}
return this
}
/**
* Scrolls a scrollable container to the content that matches the given [matcher]. If the content
* isn't yet visible, the scrollable container will be scrolled from the start till the end till
* it finds the content we're looking for. It is not defined where in the viewport the content
* will be on success of this function, but it will be either fully within the viewport if it is
* smaller than the viewport, or it will cover the whole viewport if it is larger than the
* viewport. If it doesn't find the content, the scrollable will be left at the end of the
* content and an [AssertionError] is thrown.
*
* This action should be performed on a [node][SemanticsNodeInteraction] that is a scrollable
* container, not on a node that is part of the content of that container. If the container is a
* lazy container, it must support the semantics actions [ScrollToIndex], [ScrollBy], and either
* [HorizontalScrollAxisRange] or [VerticalScrollAxisRange], for example
* [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] and
* [LazyRow][androidx.compose.foundation.lazy.LazyRow]. If the container is not lazy, it must
* support the semantics action [ScrollBy], for example,
* [Row][androidx.compose.foundation.layout.Row] or
* [Column][androidx.compose.foundation.layout.Column].
*
* Throws an [AssertionError] if the scrollable node doesn't support the necessary semantics
* actions.
*
* @param matcher A matcher that identifies the content where the scrollable container needs to
* scroll to
* @return The [SemanticsNodeInteraction] that is the receiver of this method. Note that this is
* _not_ an interaction for the node that is identified by the [matcher].
*
* @see hasScrollToNodeAction
*/
fun SemanticsNodeInteraction.performScrollToNode(
matcher: SemanticsMatcher
): SemanticsNodeInteraction {
var node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
matcher.findMatchInDescendants(node)?.also {
@OptIn(InternalTestApi::class)
it.scrollToNode(testContext.testOwner)
return this
}
// If this is NOT a lazy list, but we haven't found the node above ..
if (!node.isLazyList) {
// .. throw an error that the node doesn't exist
val msg = "No node found that matches ${matcher.description} in scrollable container"
throw AssertionError(buildGeneralErrorMessage(msg, selector, node))
}
// Go to start of the list
if (!node.horizontalScrollAxis.isAtStart || !node.verticalScrollAxis.isAtStart) {
node.scrollToIndex(0, this)
}
while (true) {
// Fetch the node again
node = fetchSemanticsNode("Failed: performScrollToNode(${matcher.description})")
matcher.findMatchInDescendants(node)?.also {
@OptIn(InternalTestApi::class)
it.scrollToNode(testContext.testOwner)
return this
}
// Are we there yet? Are we there yet? Are we there yet?
if (node.horizontalScrollAxis.isAtEnd && node.verticalScrollAxis.isAtEnd) {
// If we're finished and we haven't found the node
val msg = "No node found that matches ${matcher.description} in scrollable container"
throw AssertionError(buildGeneralErrorMessage(msg, selector, node))
}
val viewPortSize = node.layoutInfo.coordinates.boundsInParent().size
val dx = node.horizontalScrollAxis?.let { viewPortSize.width } ?: 0f
val dy = node.verticalScrollAxis?.let { viewPortSize.height } ?: 0f
// Scroll one screen
@OptIn(InternalTestApi::class)
testContext.testOwner.runOnUiThread {
node.config[ScrollBy].action?.invoke(dx, dy)
}
}
}
/**
* Executes the (partial) gesture specified in the given [block]. The gesture doesn't need to be
* complete and can be resumed in a later invocation of [performGesture]. The event time is
* initialized to the current time of the [MainTestClock].
*
* Be aware that if you split a gesture over multiple invocations of [performGesture], everything
* that happens in between will run as if the gesture is still ongoing (imagine a finger still
* touching the screen).
*
* All events that are injected from the [block] are batched together and sent after [block] is
* complete. This method blocks while the events are injected. If an error occurs during
* execution of [block] or injection of the events, all (subsequent) events are dropped and the
* error is thrown here.
*
* Example usage:
* ```
* testRule.onNodeWithTag("myWidget")
* .performGesture { swipeUp() }
*
* testRule.onNodeWithTag("myWidget")
* .performGesture { click(center) }
*
* testRule.onNodeWithTag("myWidget")
* .performGesture { down(topLeft) }
* .assertHasClickAction()
* .performGesture { up(topLeft) }
*
* testRule.onNodeWithTag("myWidget")
* .performGesture { click() }
* testRule.mainClock.advanceTimeBy(100)
* testRule.onNodeWithTag("myWidget")
* .performGesture(true) { swipeUp() }
* ```
*
* @param block A lambda with [GestureScope] as receiver that describes the gesture by
* sending all touch events.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*/
@Deprecated(
message = "Replaced by performTouchInput",
replaceWith = ReplaceWith(
"performTouchInput(block)",
"import androidx.compose.ui.test.performGesture"
)
)
@Suppress("DEPRECATION")
fun SemanticsNodeInteraction.performGesture(
block: GestureScope.() -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to perform a gesture.")
with(GestureScope(node, testContext)) {
try {
block()
} finally {
delegateScope.dispose()
}
}
return this
}
/**
* Executes the touch gesture specified in the given [block]. The gesture doesn't need to be
* complete and can be resumed in a later invocation of one of the `perform.*Input` methods. The
* event time is initialized to the current time of the [MainTestClock].
*
* Be aware that if you split a gesture over multiple invocations of `perform.*Input`, everything
* that happens in between will run as if the gesture is still ongoing (imagine a finger still
* touching the screen).
*
* All events that are injected from the [block] are batched together and sent after [block] is
* complete. This method blocks while the events are injected. If an error occurs during
* execution of [block] or injection of the events, all (subsequent) events are dropped and the
* error is thrown here.
*
* Example usage:
* ```
* // Perform a swipe up
* testRule.onNodeWithTag("myWidget")
* .performTouchInput { swipeUp() }
*
* // Perform a click off-center
* testRule.onNodeWithTag("myWidget")
* .performTouchInput { click(percentOffset(.2f, .5f) }
*
* // Do an assertion while performing a click
* testRule.onNodeWithTag("myWidget")
* .performTouchInput { down(topLeft) }
* .assertHasClickAction()
* .performTouchInput { up(topLeft) }
*
* // Perform a click-and-drag
* testRule.onNodeWithTag("myWidget").performTouchInput {
* click()
* advanceEventTime(100)
* swipeUp()
* }
* ```
*
* @param block A lambda with [TouchInjectionScope] as receiver that describes the gesture by
* sending all touch events.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @see TouchInjectionScope
*/
fun SemanticsNodeInteraction.performTouchInput(
block: TouchInjectionScope.() -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to inject touch input.")
with(MultiModalInjectionScopeImpl(node, testContext)) {
try {
block.invoke(Touch)
} finally {
dispose()
}
}
return this
}
/**
* Executes the mouse gesture specified in the given [block]. The gesture doesn't need to be
* complete and can be resumed in a later invocation of one of the `perform.*Input` methods. The
* event time is initialized to the current time of the [MainTestClock].
*
* Be aware that if you split a gesture over multiple invocations of `perform.*Input`, everything
* that happens in between will run as if the gesture is still ongoing (imagine a mouse button
* still being pressed).
*
* All events that are injected from the [block] are batched together and sent after [block] is
* complete. This method blocks while the events are injected. If an error occurs during
* execution of [block] or injection of the events, all (subsequent) events are dropped and the
* error is thrown here.
*
* Example usage:
* ```
* onNodeWithTag("myWidget")
* .performMouseInput {
* click(center)
* }
*
* onNodeWithTag("myWidget")
* // Scroll down while the primary mouse button is down:
* .performMouseInput {
* down()
* repeat(6) {
* advanceEventTime()
* scroll(-1f)
* }
* advanceEventTime()
* up()
* }
* ```
*
* @param block A lambda with [MouseInjectionScope] as receiver that describes the gesture by
* sending all mouse events.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @see MouseInjectionScope
*/
@ExperimentalTestApi
fun SemanticsNodeInteraction.performMouseInput(
block: MouseInjectionScope.() -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to inject mouse input.")
with(MultiModalInjectionScopeImpl(node, testContext)) {
try {
block.invoke(Mouse)
} finally {
dispose()
}
}
return this
}
/**
* Executes the multi-modal gesture specified in the given [block]. The gesture doesn't need to be
* complete and can be resumed in a later invocation of one of the `perform.*Input` methods. The
* event time is initialized to the current time of the [MainTestClock]. If only a single
* modality is needed (e.g. touch, mouse, stylus, keyboard, etc), you should use the
* `perform.*Input` of that modality instead.
*
* Each input modality is made available via a property of that modality's scope type, like
* [Touch][MultiModalInjectionScope.Touch] of type [TouchInjectionScope]. This allows you to
* inject events for each modality.
*
* Be aware that if you split a gesture over multiple invocations of `perform.*Input`, everything
* that happens in between will run as if the gesture is still ongoing (imagine a finger still
* touching the screen).
*
* All events that are injected from the [block] are batched together and sent after [block] is
* complete. This method blocks while the events are injected. If an error occurs during
* execution of [block] or injection of the events, all (subsequent) events are dropped and the
* error is thrown here.
*
* @param block A lambda with [MultiModalInjectionScope] as receiver that describes the gesture
* by sending all multi modal events.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @see MultiModalInjectionScope
*/
// TODO(fresen): add example of multi-modal input when Keyboard input is added (touch and mouse
// don't work together, so an example with those two doesn't make sense)
fun SemanticsNodeInteraction.performMultiModalInput(
block: MultiModalInjectionScope.() -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to inject multi-modal input.")
with(MultiModalInjectionScopeImpl(node, testContext)) {
try {
block.invoke(this)
} finally {
dispose()
}
}
return this
}
@Deprecated(
message = "Replaced with same function, but with SemanticsNodeInteraction as return type",
level = DeprecationLevel.HIDDEN
)
@Suppress("unused")
@JvmName("performSemanticsAction")
fun > SemanticsNodeInteraction.performSemanticsActionUnit(
key: SemanticsPropertyKey>,
invocation: (T) -> Unit
) {
performSemanticsAction(key, invocation)
}
/**
* 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.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @throws AssertionError If the semantics action is not defined on this node.
*/
fun > SemanticsNodeInteraction.performSemanticsAction(
key: SemanticsPropertyKey>,
invocation: (T) -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to perform ${key.name} action.")
requireSemantics(node, key) {
"Failed to perform action ${key.name}"
}
@OptIn(InternalTestApi::class)
testContext.testOwner.runOnUiThread {
node.config[key].action?.let(invocation)
}
return this
}
@Deprecated(
message = "Replaced with same function, but with SemanticsNodeInteraction as return type",
level = DeprecationLevel.HIDDEN
)
@Suppress("unused")
@JvmName("performSemanticsAction")
fun SemanticsNodeInteraction.performSemanticsActionUnit(
key: SemanticsPropertyKey Boolean>>
) {
performSemanticsAction(key)
}
/**
* 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.
* @return The [SemanticsNodeInteraction] that is the receiver of this method
*
* @throws AssertionError If the semantics action is not defined on this node.
*/
fun SemanticsNodeInteraction.performSemanticsAction(
key: SemanticsPropertyKey Boolean>>
): SemanticsNodeInteraction {
return performSemanticsAction(key) { it.invoke() }
}
// TODO(200928505): get a more accurate indication if it is a lazy list
private val SemanticsNode.isLazyList: Boolean
get() = ScrollBy in config && ScrollToIndex in config
private val SemanticsNode.horizontalScrollAxis: ScrollAxisRange?
get() = config.getOrNull(HorizontalScrollAxisRange)
private val SemanticsNode.verticalScrollAxis: ScrollAxisRange?
get() = config.getOrNull(VerticalScrollAxisRange)
private val SemanticsNode.isReversedHorizontally: Boolean
get() = horizontalScrollAxis?.reverseScrolling ?: false
private val SemanticsNode.isReversedVertically: Boolean
get() = verticalScrollAxis?.reverseScrolling ?: false
private val ScrollAxisRange?.isAtStart: Boolean
get() = this?.let { value() == 0f } ?: true
private val ScrollAxisRange?.isAtEnd: Boolean
get() = this?.let { value() == maxValue() } ?: true
private val SemanticsNode.isRtl: Boolean
get() = layoutInfo.layoutDirection == LayoutDirection.Rtl
private fun SemanticsNodeInteraction.requireSemantics(
node: SemanticsNode,
vararg properties: SemanticsPropertyKey<*>,
errorMessage: () -> String
) {
val missingProperties = properties.filter { it !in node.config }
if (missingProperties.isNotEmpty()) {
val msg = "${errorMessage()}, the node is missing [${missingProperties.joinToString()}]"
throw AssertionError(buildGeneralErrorMessage(msg, selector, node))
}
}
@Suppress("NOTHING_TO_INLINE") // Avoids doubling the stack depth for recursive search
private inline fun SemanticsMatcher.findMatchInDescendants(root: SemanticsNode): SemanticsNode? {
return root.children.firstOrNull { it.layoutInfo.isPlaced && findMatchInHierarchy(it) != null }
}
private fun SemanticsMatcher.findMatchInHierarchy(node: SemanticsNode): SemanticsNode? {
return if (matches(node)) node else findMatchInDescendants(node)
}