Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setOrientation command #2121

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions maestro-client/src/main/java/maestro/DeviceOrientation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package maestro

enum class DeviceOrientation {
PORTRAIT,
LANDSCAPE_LEFT,
LANDSCAPE_RIGHT,
UPSIDE_DOWN;

// Return the camelCase representation of the enum name, for example "landscapeLeft"
val camelCaseName: String
get() = name.split("_")
.mapIndexed { index, part ->
if (index == 0) part.lowercase()
else part.lowercase().capitalize()
}
.joinToString("")

companion object {
// Support lookup of enum value by name, ignoring underscores and case. This allow inputs like
// "LANDSCAPE_LEFT" or "landscapeLeft" to both be matched to the LANDSCAPE_LEFT enum value.
fun getByName(name: String): DeviceOrientation? {
return values().find {
comparableName(it.name) == comparableName(name)
}
}

private fun comparableName(name: String): String {
return name.lowercase().replace("_", "")
}
}
}
2 changes: 2 additions & 0 deletions maestro-client/src/main/java/maestro/Driver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ interface Driver {

fun setLocation(latitude: Double, longitude: Double)

fun setOrientation(orientation: DeviceOrientation)

fun eraseText(charactersToErase: Int)

fun setProxy(host: String, port: Int)
Expand Down
10 changes: 10 additions & 0 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,16 @@ class Maestro(
driver.setLocation(latitude.toDouble(), longitude.toDouble())
}

fun setOrientation(orientation: DeviceOrientation, waitForAppToSettle: Boolean = true) {
LOGGER.info("Setting orientation: $orientation")

driver.setOrientation(orientation)

if (waitForAppToSettle) {
waitForAppToSettle()
}
}

fun eraseText(charactersToErase: Int) {
LOGGER.info("Erasing $charactersToErase characters")

Expand Down
12 changes: 12 additions & 0 deletions maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ class AndroidDriver(
}
}

override fun setOrientation(orientation: DeviceOrientation) {
// Disable accelerometer based rotation before overriding orientation
dadb.shell("settings put system accelerometer_rotation 0")
Copy link

@vbarthel-fr vbarthel-fr Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random thought - not sure it's something we should care about - but from my understanding the "auto-rotate" that is disabled here is never re-enabled.

That also seems to be true for the user_rotation system settings: the last setOrientation call will "persist" the chosen user_rotation system setting beyond the current running flow.

If someone is reusing the same android device to run multiple flows one after the other, it might introduce some side effect between them: if one flow is using setOrientation commands while another expect the orientation to be in "auto-rotate" mode (or expect a "default" orientation).

Edit: if this "side effect" is not something desired, it might be possible to mimic what is done for the setProxy: if set, proxy is "reset" when AndroidDriver is closed:


(setProxy is also using settings under the hood)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @vbarthel-fr 👋 first off, thanks so much pulling and performing an android test for me!

You bring up a good topic here that I hadn't thought about yet. (And thanks for the pointer to resetProxy as an example being called from close().)

Do you foresee any holes in doing something like this:

  1. The first time that setOrientation is called for a flow we store the current values of accelerometer_rotation and user_rotation (and for iOS just the XCUIDevice.shared.orientation)
  2. From the close(), if those were saved we reset them to the original values.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that sounds good to me :)

But I'm not knowledgeable enough to know for sure that is OK to rely on AndroidDriver.close(). Tomorrow, I will try to have a closer look at the source code, starting by understanding when close() is called.


when(orientation) {
DeviceOrientation.PORTRAIT -> dadb.shell("settings put system user_rotation 0")
DeviceOrientation.LANDSCAPE_LEFT -> dadb.shell("settings put system user_rotation 1")
DeviceOrientation.UPSIDE_DOWN -> dadb.shell("settings put system user_rotation 2")
DeviceOrientation.LANDSCAPE_RIGHT -> dadb.shell("settings put system user_rotation 3")
}
}

override fun eraseText(charactersToErase: Int) {
runDeviceCall {
blockingStubWithTimeout.eraseAllText(
Expand Down
6 changes: 6 additions & 0 deletions maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@ class IOSDriver(
iosDevice.setLocation(latitude, longitude).expect {}
}

override fun setOrientation(orientation: DeviceOrientation) {
runDeviceCall {
iosDevice.setOrientation(orientation.camelCaseName)
}
}

override fun eraseText(charactersToErase: Int) {
runDeviceCall { iosDevice.eraseText(charactersToErase) }
}
Expand Down
5 changes: 5 additions & 0 deletions maestro-client/src/main/java/maestro/drivers/WebDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package maestro.drivers

import maestro.Capability
import maestro.DeviceInfo
import maestro.DeviceOrientation
import maestro.Driver
import maestro.KeyCode
import maestro.Maestro
Expand Down Expand Up @@ -343,6 +344,10 @@ class WebDriver(val isStudio: Boolean) : Driver {
TODO("Not yet implemented")
}

override fun setOrientation(orientation: DeviceOrientation) {
// no-op for web
}

override fun eraseText(charactersToErase: Int) {
val driver = ensureOpen()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ class XCTestDriverClient(
))
}

fun setOrientation(orientation: String) {
executeJsonRequest("setOrientation", SetOrientationRequest(orientation))
}

fun pressKey(name: String) {
executeJsonRequest("pressKey", PressKeyRequest(name))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package xcuitest.api

data class SetOrientationRequest(val orientation: String)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
52F0B1B32B3C26DF00C6471A /* KeyboardHandlerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0B1B22B3C26DF00C6471A /* KeyboardHandlerRequest.swift */; };
52F0B1B52B3C27F700C6471A /* KeyboardHandlerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0B1B42B3C27F700C6471A /* KeyboardHandlerResponse.swift */; };
52F33A942AE6823100692902 /* StatusResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F33A932AE6823100692902 /* StatusResponse.swift */; };
5B8E0ABC2CD562D000E9D439 /* SetOrientationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8E0ABB2CD562D000E9D439 /* SetOrientationHandler.swift */; };
5B8E0ABE2CD562F200E9D439 /* SetOrientationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8E0ABD2CD562F200E9D439 /* SetOrientationRequest.swift */; };
610D58F92A45B5DA00B4BBEB /* ViewHierarchyHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610D58F82A45B5DA00B4BBEB /* ViewHierarchyHandler.swift */; };
610D58FB2A49E3CA00B4BBEB /* AXClientSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610D58FA2A49E3CA00B4BBEB /* AXClientSwizzler.swift */; };
6124329C2A4B368100F5F619 /* ViewHierarchyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6124329B2A4B368100F5F619 /* ViewHierarchyRequest.swift */; };
Expand Down Expand Up @@ -98,6 +100,8 @@
52F0B1B22B3C26DF00C6471A /* KeyboardHandlerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardHandlerRequest.swift; sourceTree = "<group>"; };
52F0B1B42B3C27F700C6471A /* KeyboardHandlerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardHandlerResponse.swift; sourceTree = "<group>"; };
52F33A932AE6823100692902 /* StatusResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusResponse.swift; sourceTree = "<group>"; };
5B8E0ABB2CD562D000E9D439 /* SetOrientationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetOrientationHandler.swift; sourceTree = "<group>"; };
5B8E0ABD2CD562F200E9D439 /* SetOrientationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetOrientationRequest.swift; sourceTree = "<group>"; };
610D58F82A45B5DA00B4BBEB /* ViewHierarchyHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHierarchyHandler.swift; sourceTree = "<group>"; };
610D58FA2A49E3CA00B4BBEB /* AXClientSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXClientSwizzler.swift; sourceTree = "<group>"; };
6124329B2A4B368100F5F619 /* ViewHierarchyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHierarchyRequest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -226,6 +230,7 @@
32097964297092A800340282 /* InputTextRequest.swift */,
32ECCB252980449200A1A0A0 /* TouchRequest.swift */,
61C0AFE429C7AAB3005D1FC5 /* PressKeyRequest.swift */,
5B8E0ABD2CD562F200E9D439 /* SetOrientationRequest.swift */,
61C0AFE829C86378005D1FC5 /* PressButtonRequest.swift */,
61C0AFEE29C8961F005D1FC5 /* EraseTextRequest.swift */,
61C0AFF229C8C040005D1FC5 /* DeviceInfoResponse.swift */,
Expand Down Expand Up @@ -268,6 +273,7 @@
61C0AFEA29C863BB005D1FC5 /* PressButtonHandler.swift */,
61C0AFEC29C88926005D1FC5 /* EraseTextHandler.swift */,
61C0AFF029C8C01F005D1FC5 /* DeviceInfoHandler.swift */,
5B8E0ABB2CD562D000E9D439 /* SetOrientationHandler.swift */,
945DD44C29D6F73B004D8ECF /* SetPermissionsHandler.swift */,
52047F772A7A638E00BF982D /* StatusHandler.swift */,
52F0B1B02B3C25BC00C6471A /* KeyboardRouteHandler.swift */,
Expand Down Expand Up @@ -429,10 +435,12 @@
52F33A942AE6823100692902 /* StatusResponse.swift in Sources */,
610D58FB2A49E3CA00B4BBEB /* AXClientSwizzler.swift in Sources */,
F328D3E62A2A98E7000546D3 /* StringExtensions.swift in Sources */,
5B8E0ABC2CD562D000E9D439 /* SetOrientationHandler.swift in Sources */,
61C0AFEF29C8961F005D1FC5 /* EraseTextRequest.swift in Sources */,
61A79B9729DF0B8A00C38882 /* SwipeRouteHandlerV2.swift in Sources */,
52F0B1B52B3C27F700C6471A /* KeyboardHandlerResponse.swift in Sources */,
52047F782A7A638E00BF982D /* StatusHandler.swift in Sources */,
5B8E0ABE2CD562F200E9D439 /* SetOrientationRequest.swift in Sources */,
945DD44B29D6F5D8004D8ECF /* SetPermissionsRequest.swift in Sources */,
613E87DF299BE78400FF8551 /* KeyModifierFlags.swift in Sources */,
94A90DDE298AE72A006EB769 /* XCUIElement+Extensions.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import FlyingFox
import os
import XCTest

@MainActor
struct SetOrientationHandler: HTTPHandler {
private let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: Self.self)
)

func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse {
guard let requestBody = try? JSONDecoder().decode(SetOrientationRequest.self, from: request.body) else {
return AppError(type: .precondition, message: "incorrect request body provided for set orientation").httpResponse
}

XCUIDevice.shared.orientation = requestBody.orientation.uiDeviceOrientation
return HTTPResponse(statusCode: .ok)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import UIKit

struct SetOrientationRequest: Codable {
let orientation: Orientation

enum Orientation: String, Codable {
case portrait
case landscapeLeft
case landscapeRight
case upsideDown

var uiDeviceOrientation: UIDeviceOrientation {
switch self {
case .portrait:
return .portrait
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
case .upsideDown:
return .portraitUpsideDown
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class RouteHandlerFactory {
return EraseTextHandler()
case .deviceInfo:
return DeviceInfoHandler()
case .setOrientation:
return SetOrientationHandler()
case .setPermissions:
return SetPermissionsHandler()
case .viewHierarchy:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum Route: String, CaseIterable {
case pressButton
case eraseText
case deviceInfo
case setOrientation
case setPermissions
case viewHierarchy
case status
Expand Down
7 changes: 7 additions & 0 deletions maestro-ios/src/main/java/ios/IOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ interface IOSDevice : AutoCloseable {
*/
fun setLocation(latitude: Double, longitude: Double): Result<Unit, Throwable>

/**
* Sets the device's orientation.
*
* @param link - link to open
*/
fun setOrientation(orientation: String)

/**
* @return true if the connection to the device (not device itself) is shut down
*/
Expand Down
4 changes: 4 additions & 0 deletions maestro-ios/src/main/java/ios/LocalIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class LocalIOSDevice(
return simctlIOSDevice.setLocation(latitude, longitude)
}

override fun setOrientation(orientation: String) {
return xcTestDevice.setOrientation(orientation)
}

override fun isShutdown(): Boolean {
return xcTestDevice.isShutdown()
}
Expand Down
4 changes: 4 additions & 0 deletions maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ class SimctlIOSDevice(
}
}

override fun setOrientation(orientation: String) {
TODO("Not yet implemented")
}

override fun isShutdown(): Boolean {
TODO("Not yet implemented")
}
Expand Down
4 changes: 4 additions & 0 deletions maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ class XCTestIOSDevice(
error("Not supported")
}

override fun setOrientation(orientation: String) {
execute { client.setOrientation(orientation) }
}

override fun isShutdown(): Boolean {
return !client.isChannelAlive()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package maestro.orchestra

import maestro.DeviceOrientation
import maestro.KeyCode
import maestro.Point
import maestro.ScrollDirection
Expand Down Expand Up @@ -758,6 +759,21 @@ data class SetLocationCommand(
}
}

data class SetOrientationCommand(
val orientation: DeviceOrientation,
override val label: String? = null,
override val optional: Boolean = false,
) : Command {

override fun description(): String {
return label ?: "Set orientation ${orientation}"
}

override fun evaluateScripts(jsEngine: JsEngine): SetOrientationCommand {
return this
}
}

data class RepeatCommand(
val times: String? = null,
val condition: Condition? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ data class MaestroCommand(
val clearKeychainCommand: ClearKeychainCommand? = null,
val runFlowCommand: RunFlowCommand? = null,
val setLocationCommand: SetLocationCommand? = null,
var setOrientationCommand: SetOrientationCommand? = null,
val repeatCommand: RepeatCommand? = null,
val copyTextCommand: CopyTextFromCommand? = null,
val pasteTextCommand: PasteTextCommand? = null,
Expand Down Expand Up @@ -95,6 +96,7 @@ data class MaestroCommand(
clearKeychainCommand = command as? ClearKeychainCommand,
runFlowCommand = command as? RunFlowCommand,
setLocationCommand = command as? SetLocationCommand,
setOrientationCommand = command as? SetOrientationCommand,
repeatCommand = command as? RepeatCommand,
copyTextCommand = command as? CopyTextFromCommand,
pasteTextCommand = command as? PasteTextCommand,
Expand Down Expand Up @@ -137,6 +139,7 @@ data class MaestroCommand(
clearKeychainCommand != null -> clearKeychainCommand
runFlowCommand != null -> runFlowCommand
setLocationCommand != null -> setLocationCommand
setOrientationCommand != null -> setOrientationCommand
repeatCommand != null -> repeatCommand
copyTextCommand != null -> copyTextCommand
pasteTextCommand != null -> pasteTextCommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ class Orchestra(
is ClearKeychainCommand -> clearKeychainCommand()
is RunFlowCommand -> runFlowCommand(command, config)
is SetLocationCommand -> setLocationCommand(command)
is SetOrientationCommand -> setOrientationCommand(command)
is RepeatCommand -> repeatCommand(command, maestroCommand, config)
is DefineVariablesCommand -> defineVariablesCommand(command)
is RunScriptCommand -> runScriptCommand(command)
Expand Down Expand Up @@ -449,6 +450,12 @@ class Orchestra(
return true
}

private fun setOrientationCommand(command: SetOrientationCommand): Boolean {
maestro.setOrientation(command.orientation)

return true
}

private fun clearAppStateCommand(command: ClearStateCommand): Boolean {
maestro.clearAppState(command.appId)
// Android's clear command also resets permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator
import maestro.DeviceOrientation
import maestro.KeyCode
import maestro.Point
import maestro.TapRepeat
Expand Down Expand Up @@ -67,6 +68,7 @@ data class YamlFluentCommand(
val clearState: YamlClearState? = null,
val runFlow: YamlRunFlow? = null,
val setLocation: YamlSetLocation? = null,
val setOrientation: YamlSetOrientation? = null,
val repeat: YamlRepeatCommand? = null,
val copyTextFrom: YamlElementSelectorUnion? = null,
val runScript: YamlRunScript? = null,
Expand Down Expand Up @@ -206,6 +208,15 @@ data class YamlFluentCommand(
)
)
)
setOrientation != null -> listOf(
MaestroCommand(
SetOrientationCommand(
orientation = DeviceOrientation.getByName(setOrientation.orientation) ?: throw SyntaxError("Unknown orientation: $setOrientation"),
label = setOrientation.label,
optional = setOrientation.optional,
)
)
)
repeat != null -> listOf(
repeatCommand(repeat, flowPath, appId)
)
Expand Down
Loading
Loading