diff --git a/e2e/workspaces/demo_app/swipe_from_to.yaml b/e2e/workspaces/demo_app/swipe_from_to.yaml new file mode 100644 index 0000000000..a8800a3a67 --- /dev/null +++ b/e2e/workspaces/demo_app/swipe_from_to.yaml @@ -0,0 +1,58 @@ +appId: com.example.example +tags: + - passing +--- +- launchApp: + clearState: true +- tapOn: + text: Swipe Test (from_to) +- assertVisible: + text: "Selected: Item 1" +- swipe: + from: + id: "scrollWheel" + to: 50%, 47% # x, y +- assertVisible: + text: "Selected: Item 2" +- swipe: + from: + id: "scrollWheel" + to: 50%, 47% # x, y +- assertVisible: + text: "Selected: Item 3" +- swipe: + from: + id: "scrollWheel" + to: 50%, 47% # x, y +- assertVisible: + text: "Selected: Item 4" +- swipe: + from: + id: "scrollWheel" + to: 50%, 47% # x, y +- assertVisible: + text: "Selected: Item 5" +- swipe: + from: + id: "scrollWheel" + to: 50%, 58% # x, y +- assertVisible: + text: "Selected: Item 4" +- swipe: + from: + id: "scrollWheel" + to: 50%, 58% # x, y +- assertVisible: + text: "Selected: Item 3" +- swipe: + from: + id: "scrollWheel" + to: 50%, 58% # x, y +- assertVisible: + text: "Selected: Item 2" +- swipe: + from: + id: "scrollWheel" + to: 50%, 58% # x, y +- assertVisible: + text: "Selected: Item 1" \ No newline at end of file diff --git a/maestro-client/src/main/java/maestro/Maestro.kt b/maestro-client/src/main/java/maestro/Maestro.kt index d369f7f89e..15c19a957d 100644 --- a/maestro-client/src/main/java/maestro/Maestro.kt +++ b/maestro-client/src/main/java/maestro/Maestro.kt @@ -164,6 +164,30 @@ class Maestro( waitForAppToSettle(waitToSettleTimeoutMs = waitToSettleTimeoutMs) } + fun swipeFromElementToPoint( + uiElement: UiElement, + endPoint: Point? = null, + endRelative: String? = null, + duration: Long = 300L + ) { + val deviceInfo = deviceInfo() + + when { + endPoint != null -> driver.swipe(uiElement.bounds.center(), endPoint, duration) + endRelative != null -> { + val endPoints = endRelative.replace("%", "") + .split(",").map { it.trim().toInt() } + val endX = deviceInfo.widthGrid * endPoints[0] / 100 + val endY = deviceInfo.heightGrid * endPoints[1] / 100 + val end = Point(endX, endY) + + driver.swipe(uiElement.bounds.center(), end, duration) + } + } + + waitForAppToSettle () + } + fun scrollVertical() { LOGGER.info("Scrolling vertically") diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt index b8fedd6b41..210d4bcec8 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt @@ -78,6 +78,9 @@ data class SwipeCommand( startRelative != null && endRelative != null -> { "Swipe from ($startRelative) to ($endRelative) in $duration ms" } + elementSelector != null && (endPoint != null || endRelative != null) -> { + "Swiping from ${elementSelector.description()} to ${endPoint ?: endRelative} coordinates" + } else -> "Invalid input to swipe command" } diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt index cff0cee995..68317f5345 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt @@ -1374,7 +1374,21 @@ class Orchestra( duration = command.duration, waitToSettleTimeoutMs = command.waitToSettleTimeoutMs ) - + elementSelector != null && (end != null || endRelative != null) -> { + val uiElement = findElement(elementSelector, optional = command.optional) + if (end != null) + maestro.swipeFromElementToPoint( + uiElement = uiElement.element, + endPoint = end, + duration = command.duration + ) + else + maestro.swipeFromElementToPoint( + uiElement = uiElement.element, + endRelative = endRelative, + duration = command.duration + ) + } else -> error("Illegal arguments for swiping") } return true diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt index 9732b17e7d..dda21a4989 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt @@ -839,6 +839,7 @@ data class YamlFluentCommand( } is YamlSwipeElement -> return swipeElementCommand(swipe) + is YamlRelativeCoordinateSwipeElement -> return swipeRelativeCoordinatesSwipeElementCommand(swipe) else -> { throw IllegalStateException( "Provide swipe direction UP, DOWN, RIGHT OR LEFT or by giving explicit " + @@ -861,6 +862,18 @@ data class YamlFluentCommand( ) } + private fun swipeRelativeCoordinatesSwipeElementCommand(swipeElement: YamlRelativeCoordinateSwipeElement): MaestroCommand { + return MaestroCommand( + swipeCommand = SwipeCommand( + elementSelector = toElementSelector(swipeElement.from), + endRelative = swipeElement.to, + duration = swipeElement.duration, + label = swipeElement.label, + optional = swipeElement.optional, + ) + ) + } + private fun toElementSelector(selectorUnion: YamlElementSelectorUnion): ElementSelector { return if (selectorUnion is StringElementSelector) { ElementSelector( diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSwipe.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSwipe.kt index 5ffac7b5ef..7a9b9609cf 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSwipe.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSwipe.kt @@ -55,6 +55,15 @@ data class YamlSwipeElement( override val waitToSettleTimeoutMs: Int? = null, ) : YamlSwipe +data class YamlRelativeCoordinateSwipeElement( + val from: YamlElementSelectorUnion, + val to: String, + override val duration: Long = DEFAULT_DURATION_IN_MILLIS, + override val label: String? = null, + override val optional: Boolean, + override val waitToSettleTimeoutMs: Int? = null, +) : YamlSwipe + private const val DEFAULT_DURATION_IN_MILLIS = 400L class YamlSwipeDeserializer : JsonDeserializer() { @@ -68,7 +77,7 @@ class YamlSwipeDeserializer : JsonDeserializer() { val optional = getOptional(root) val waitToSettleTimeoutMs = getWaitToSettleTimeoutMs(root) when { - input.contains("start") || input.contains("end") -> { + input.contains("start") && input.contains("end") -> { check(root.get("direction") == null) { "You cannot provide direction with start/end swipe." } check(root.get("start") != null && root.get("end") != null) { "You need to provide both start and end coordinates, to swipe with coordinates" @@ -94,18 +103,53 @@ class YamlSwipeDeserializer : JsonDeserializer() { mapper.convertValue(root, YamlSwipeElement::class.java) } } + input.contains("from") && input.contains("to") -> { + return resolveRelativeCoordinateSwipeElement(root, duration, label, optional, mapper) + } else -> { throw IllegalArgumentException( "Swipe command takes either: \n" + - "\t1. direction: Direction based swipe with: \"RIGHT\", \"LEFT\", \"UP\", or \"DOWN\" or \n" + - "\t2. start and end: Coordinates based swipe with: \"start\" and \"end\" coordinates \n" + - "\t3. direction and element to swipe directionally on element\n" + - "It seems you provided invalid input with: $input" + "\t1. direction: Direction based swipe with: \"RIGHT\", \"LEFT\", \"UP\", or \"DOWN\" or \n" + + "\t2. start and end: Coordinates based swipe with: \"start\" and \"end\" coordinates \n" + + "\t3. direction and element to swipe directionally on element\n" + + "\t4. from and direction/to: more precise swipe from one point to another\n" + + + "It seems you provided invalid input with: $input" ) } } } + private fun resolveRelativeCoordinateSwipeElement( + root: TreeNode, + duration: Long, + label: String?, + optional: Boolean, + mapper: ObjectMapper + ): YamlRelativeCoordinateSwipeElement { + val from = mapper.convertValue(root.path("from"), YamlElementSelectorUnion::class.java) + val to = root.path("to").toString().replace("\"", "") + + val isRelative = to.contains("%") + + if (isRelative) { + val endPoints = to + .replace("%", "") + .split(",") + .map { it.trim().toInt() } + check(endPoints[0] in 0..100 && endPoints[1] in 0..100) { + "Invalid end point: $to should be between 0 to 100 when using relative coordinates." + } + } else { + val endPoints = to + .split(",") + .map { it.trim().toInt() } + check(endPoints.size == 2) { "Invalid format for absolute coordinates: $to" } + } + + return YamlRelativeCoordinateSwipeElement(from, to, duration, label, optional) + } + private fun resolveCoordinateSwipe( root: TreeNode, duration: Long,