diff --git a/maestro-client/src/main/java/maestro/DeviceOrientation.kt b/maestro-client/src/main/java/maestro/DeviceOrientation.kt new file mode 100644 index 0000000000..191e23324c --- /dev/null +++ b/maestro-client/src/main/java/maestro/DeviceOrientation.kt @@ -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("_", "") + } + } +} diff --git a/maestro-client/src/main/java/maestro/Driver.kt b/maestro-client/src/main/java/maestro/Driver.kt index 1abcd8fdd9..9f49d67e87 100644 --- a/maestro-client/src/main/java/maestro/Driver.kt +++ b/maestro-client/src/main/java/maestro/Driver.kt @@ -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) diff --git a/maestro-client/src/main/java/maestro/Maestro.kt b/maestro-client/src/main/java/maestro/Maestro.kt index 1876e8365c..782308eb38 100644 --- a/maestro-client/src/main/java/maestro/Maestro.kt +++ b/maestro-client/src/main/java/maestro/Maestro.kt @@ -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") diff --git a/maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt b/maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt index 1cb640093e..613d02d135 100644 --- a/maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt +++ b/maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt @@ -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") + + 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( diff --git a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt index f4ae891751..7ba737fc2c 100644 --- a/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt +++ b/maestro-client/src/main/java/maestro/drivers/IOSDriver.kt @@ -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) } } diff --git a/maestro-client/src/main/java/maestro/drivers/WebDriver.kt b/maestro-client/src/main/java/maestro/drivers/WebDriver.kt index e0b02dde7d..5fbe963875 100644 --- a/maestro-client/src/main/java/maestro/drivers/WebDriver.kt +++ b/maestro-client/src/main/java/maestro/drivers/WebDriver.kt @@ -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 @@ -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() diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt index 9f0a7cf3ed..9c4b5c74e8 100644 --- a/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt @@ -161,6 +161,10 @@ class XCTestDriverClient( )) } + fun setOrientation(orientation: String) { + executeJsonRequest("setOrientation", SetOrientationRequest(orientation)) + } + fun pressKey(name: String) { executeJsonRequest("pressKey", PressKeyRequest(name)) } diff --git a/maestro-ios-driver/src/main/kotlin/xcuitest/api/SetOrientationRequest.kt b/maestro-ios-driver/src/main/kotlin/xcuitest/api/SetOrientationRequest.kt new file mode 100644 index 0000000000..699c96ba2e --- /dev/null +++ b/maestro-ios-driver/src/main/kotlin/xcuitest/api/SetOrientationRequest.kt @@ -0,0 +1,3 @@ +package xcuitest.api + +data class SetOrientationRequest(val orientation: String) diff --git a/maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj/project.pbxproj b/maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj/project.pbxproj index 8096a1a12d..953eb0b8e6 100644 --- a/maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj/project.pbxproj +++ b/maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj/project.pbxproj @@ -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 */; }; @@ -98,6 +100,8 @@ 52F0B1B22B3C26DF00C6471A /* KeyboardHandlerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardHandlerRequest.swift; sourceTree = ""; }; 52F0B1B42B3C27F700C6471A /* KeyboardHandlerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardHandlerResponse.swift; sourceTree = ""; }; 52F33A932AE6823100692902 /* StatusResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusResponse.swift; sourceTree = ""; }; + 5B8E0ABB2CD562D000E9D439 /* SetOrientationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetOrientationHandler.swift; sourceTree = ""; }; + 5B8E0ABD2CD562F200E9D439 /* SetOrientationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetOrientationRequest.swift; sourceTree = ""; }; 610D58F82A45B5DA00B4BBEB /* ViewHierarchyHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHierarchyHandler.swift; sourceTree = ""; }; 610D58FA2A49E3CA00B4BBEB /* AXClientSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXClientSwizzler.swift; sourceTree = ""; }; 6124329B2A4B368100F5F619 /* ViewHierarchyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHierarchyRequest.swift; sourceTree = ""; }; @@ -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 */, @@ -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 */, @@ -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 */, diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/SetOrientationHandler.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/SetOrientationHandler.swift new file mode 100644 index 0000000000..c4056a3da6 --- /dev/null +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/SetOrientationHandler.swift @@ -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) + } +} diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Models/SetOrientationRequest.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Models/SetOrientationRequest.swift new file mode 100644 index 0000000000..b917c91be6 --- /dev/null +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Models/SetOrientationRequest.swift @@ -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 + } + } + } +} diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/RouteHandlerFactory.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/RouteHandlerFactory.swift index acfcaabe0a..b2b8d97312 100644 --- a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/RouteHandlerFactory.swift +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/RouteHandlerFactory.swift @@ -26,6 +26,8 @@ class RouteHandlerFactory { return EraseTextHandler() case .deviceInfo: return DeviceInfoHandler() + case .setOrientation: + return SetOrientationHandler() case .setPermissions: return SetPermissionsHandler() case .viewHierarchy: diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/XCTestHTTPServer.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/XCTestHTTPServer.swift index 061034c4df..9abec9467b 100644 --- a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/XCTestHTTPServer.swift +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/XCTestHTTPServer.swift @@ -13,6 +13,7 @@ enum Route: String, CaseIterable { case pressButton case eraseText case deviceInfo + case setOrientation case setPermissions case viewHierarchy case status diff --git a/maestro-ios/src/main/java/ios/IOSDevice.kt b/maestro-ios/src/main/java/ios/IOSDevice.kt index 1669a8ba28..882b725706 100644 --- a/maestro-ios/src/main/java/ios/IOSDevice.kt +++ b/maestro-ios/src/main/java/ios/IOSDevice.kt @@ -131,6 +131,13 @@ interface IOSDevice : AutoCloseable { */ fun setLocation(latitude: Double, longitude: Double): Result + /** + * 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 */ diff --git a/maestro-ios/src/main/java/ios/LocalIOSDevice.kt b/maestro-ios/src/main/java/ios/LocalIOSDevice.kt index 10fdd75b9c..dc64946107 100644 --- a/maestro-ios/src/main/java/ios/LocalIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/LocalIOSDevice.kt @@ -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() } diff --git a/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt b/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt index f25e4f794c..7f1ed72345 100644 --- a/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/simctl/SimctlIOSDevice.kt @@ -152,6 +152,10 @@ class SimctlIOSDevice( } } + override fun setOrientation(orientation: String) { + TODO("Not yet implemented") + } + override fun isShutdown(): Boolean { TODO("Not yet implemented") } diff --git a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt index da2577e6d2..da5f159a2e 100644 --- a/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt +++ b/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt @@ -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() } 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 9c59306b48..775456b983 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt @@ -19,6 +19,7 @@ package maestro.orchestra +import maestro.DeviceOrientation import maestro.KeyCode import maestro.Point import maestro.ScrollDirection @@ -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, diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt index 5983b3c8fd..95dd660a18 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt @@ -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, @@ -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, @@ -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 diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt index 95c990f3d5..01cf97d617 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt @@ -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) @@ -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 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 1a55efbaf3..7eb419e9e9 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt @@ -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 @@ -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, @@ -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) ) diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSetOrientation.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSetOrientation.kt new file mode 100644 index 0000000000..ee00dccc9e --- /dev/null +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlSetOrientation.kt @@ -0,0 +1,17 @@ +package maestro.orchestra.yaml + +import com.fasterxml.jackson.annotation.JsonCreator + +data class YamlSetOrientation ( + val orientation: String, + val label: String? = null, + val optional: Boolean = false, +){ + companion object { + @JvmStatic + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + fun parse(orientation: String) = YamlSetOrientation( + orientation = orientation, + ) + } +} diff --git a/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt b/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt index 3339c9a316..771d2789d5 100644 --- a/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt +++ b/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.common.truth.Truth.assertThat +import maestro.DeviceOrientation import maestro.KeyCode import maestro.Point import org.intellij.lang.annotations.Language @@ -590,6 +591,33 @@ internal class MaestroCommandSerializationTest { .isEqualTo(command) } + @Test + fun `serialize SetOrientationCommand`() { + // given + val command = MaestroCommand( + SetOrientationCommand(DeviceOrientation.PORTRAIT) + ) + + // when + val serializedCommandJson = command.toJson() + val deserializedCommand = objectMapper.readValue(serializedCommandJson, MaestroCommand::class.java) + + // then + @Language("json") + val expectedJson = """ + { + "setOrientationCommand" : { + "orientation" : "PORTRAIT", + "optional" : false + } + } + """.trimIndent() + assertThat(serializedCommandJson) + .isEqualTo(expectedJson) + assertThat(deserializedCommand) + .isEqualTo(command) + } + private fun MaestroCommand.toJson(): String = objectMapper .writerWithDefaultPrettyPrinter() diff --git a/maestro-orchestra/src/test/java/maestro/orchestra/yaml/YamlCommandReaderTest.kt b/maestro-orchestra/src/test/java/maestro/orchestra/yaml/YamlCommandReaderTest.kt index 19db099240..0b0946249c 100644 --- a/maestro-orchestra/src/test/java/maestro/orchestra/yaml/YamlCommandReaderTest.kt +++ b/maestro-orchestra/src/test/java/maestro/orchestra/yaml/YamlCommandReaderTest.kt @@ -1,6 +1,7 @@ package maestro.orchestra.yaml import com.google.common.truth.Truth.assertThat +import maestro.DeviceOrientation import maestro.KeyCode import maestro.ScrollDirection import maestro.SwipeDirection @@ -35,6 +36,7 @@ import maestro.orchestra.PressKeyCommand import maestro.orchestra.RepeatCommand import maestro.orchestra.RunFlowCommand import maestro.orchestra.RunScriptCommand +import maestro.orchestra.SetOrientationCommand import maestro.orchestra.ScrollCommand import maestro.orchestra.ScrollUntilVisibleCommand import maestro.orchestra.SetAirplaneModeCommand @@ -427,6 +429,10 @@ internal class YamlCommandReaderTest { sourceDescription = "023_runScript_test.js", label = "Run some special calculations" ), + SetOrientationCommand( + orientation = DeviceOrientation.LANDSCAPE_LEFT, + label = "Set the device orientation" + ), ScrollCommand( label = "Scroll down" ), diff --git a/maestro-orchestra/src/test/resources/YamlCommandReaderTest/023_labels.yaml b/maestro-orchestra/src/test/resources/YamlCommandReaderTest/023_labels.yaml index 92fe7ac7ef..b00b5c70cc 100644 --- a/maestro-orchestra/src/test/resources/YamlCommandReaderTest/023_labels.yaml +++ b/maestro-orchestra/src/test/resources/YamlCommandReaderTest/023_labels.yaml @@ -81,6 +81,9 @@ appId: com.example.app - runScript: file: "023_runScript_test.js" label: "Run some special calculations" +- setOrientation: + orientation: "LANDSCAPE_LEFT" + label: "Set the device orientation" - scroll: label: "Scroll down" - scrollUntilVisible: diff --git a/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt b/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt index b6bb66a2bf..0cbe4f5554 100644 --- a/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt +++ b/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt @@ -81,6 +81,12 @@ class FakeDriver : Driver { ) } + override fun setOrientation(orientation: DeviceOrientation) { + ensureOpen() + + events += Event.SetOrientation(orientation) + } + override fun launchApp( appId: String, launchArguments: Map, @@ -455,6 +461,10 @@ class FakeDriver : Driver { val code: KeyCode, ) : Event() + data class SetOrientation( + val orientation: DeviceOrientation, + ) : Event() + object TakeScreenshot : Event() object ClearKeychain : Event() diff --git a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt index 972f1c2f81..03ab672b11 100644 --- a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt +++ b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt @@ -1,6 +1,7 @@ package maestro.test import com.google.common.truth.Truth.assertThat +import maestro.DeviceOrientation import maestro.KeyCode import maestro.Maestro import maestro.MaestroException @@ -3171,6 +3172,26 @@ class IntegrationTest { ) } + @Test + fun `Case 119 - Set orientation`() { + // Given + val commands = readCommands("119_set_orientation") + + val driver = driver { + } + + // When + Maestro(driver).use { + orchestra(it).runFlow(commands) + } + + // Then + driver.assertHasEvent(Event.SetOrientation(DeviceOrientation.PORTRAIT)) + driver.assertHasEvent(Event.SetOrientation(DeviceOrientation.LANDSCAPE_LEFT)) + driver.assertHasEvent(Event.SetOrientation(DeviceOrientation.LANDSCAPE_RIGHT)) + driver.assertHasEvent(Event.SetOrientation(DeviceOrientation.UPSIDE_DOWN)) + } + private fun orchestra( maestro: Maestro, ) = Orchestra( diff --git a/maestro-test/src/test/resources/119_set_orientation.yaml b/maestro-test/src/test/resources/119_set_orientation.yaml new file mode 100644 index 0000000000..5ee9684a74 --- /dev/null +++ b/maestro-test/src/test/resources/119_set_orientation.yaml @@ -0,0 +1,6 @@ +appId: com.example.app +--- +- setOrientation: PORTRAIT +- setOrientation: LANDSCAPE_LEFT +- setOrientation: LANDSCAPE_RIGHT +- setOrientation: UPSIDE_DOWN