Skip to content

Commit

Permalink
Adds shake command
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanacostarubio committed Jan 20, 2025
1 parent 84624cf commit 9050e69
Show file tree
Hide file tree
Showing 20 changed files with 188 additions and 1 deletion.
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 @@ -43,6 +43,8 @@ interface Driver {

fun killApp(appId: String)

fun shake()

fun clearAppState(appId: String)

fun clearKeychain()
Expand Down
6 changes: 6 additions & 0 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ class Maestro(
driver.killApp(appId)
}

fun shake(){
LOGGER.info("Shake device")

driver.shake()
}

fun clearAppState(appId: String) {
LOGGER.info("Clearing app state $appId")

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

override fun shake() {
dadb.shell("emu sensor set acceleration 100:100:100")
dadb.shell("emu sensor set acceleration 0:0:0")
}

override fun clearAppState(appId: String) {
metrics.measured("operation", mapOf("command" to "clearAppState", "appId" to appId)) {
if (!isPackageInstalled(appId)) {
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 @@ -104,6 +104,12 @@ class IOSDriver(
}
}

override fun shake() {
metrics.measured("operation", mapOf("command" to "shake")) {
iosDevice.shake()
}
}

override fun clearAppState(appId: String) {
metrics.measured("operation", mapOf("command" to "clearAppState", "appId" to appId)) {
iosDevice.clearAppState(appId)
Expand Down
4 changes: 4 additions & 0 deletions maestro-client/src/main/java/maestro/drivers/WebDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ class WebDriver(
stopApp(appId)
}

override fun shake() {
// TODO: Implement shake for web
}

override fun contentDescriptor(excludeKeyboardElements: Boolean): TreeNode {
ensureOpen()

Expand Down
38 changes: 38 additions & 0 deletions maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,44 @@ class XCTestDriverClient(
installer.close()
}

fun shake(){
logger.trace("Executing shake gesture on iOS simulator")

try {
// Create ProcessBuilder for executing AppleScript
val processBuilder = ProcessBuilder(
"osascript",
"-e", """
tell application "Simulator"
activate
delay 0.5 -- Give simulator time to come to foreground
tell application "System Events"
tell process "Simulator"
-- Access the Hardware menu and click Shake Gesture
click menu item "Shake" of menu "Device" of menu bar 1
end tell
end tell
end tell
""".trimIndent()
)

// Execute the command
val process = processBuilder.start()
val exitCode = process.waitFor()

if (exitCode != 0) {
val errorStream = process.errorStream.bufferedReader().readText()
logger.error("Failed to execute shake gesture. Exit code: $exitCode, Error: $errorStream")
throw IOException("Failed to execute shake gesture on simulator. Exit code: $exitCode")
}

logger.trace("Successfully executed shake gesture")
} catch (e: Exception) {
logger.error("Error executing shake gesture", e)
throw XCUITestServerError.UnknownFailure("Failed to execute shake gesture on simulator: ${e.message}")
}
}

fun setPermissions(permissions: Map<String, String>) {
executeJsonRequest("setPermissions", SetPermissionsRequest(permissions))
}
Expand Down
5 changes: 5 additions & 0 deletions maestro-ios/src/main/java/ios/IOSDevice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ interface IOSDevice : AutoCloseable {

fun eraseText(charactersToErase: Int)

/**
* Shake the device
*/
fun shake()

fun addMedia(path: String)
}

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 @@ -154,6 +154,10 @@ class LocalIOSDevice(
xcTestDevice.eraseText(charactersToErase)
}

override fun shake(){
xcTestDevice.shake()
}

override fun addMedia(path: String) {
simctlIOSDevice.addMedia(path)
}
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 @@ -187,6 +187,10 @@ class SimctlIOSDevice(
TODO("Not yet implemented")
}

override fun shake(){
TODO("Not yet implemented")
}

override fun close() {
stopScreenRecording()
}
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 @@ -215,6 +215,10 @@ class XCTestIOSDevice(
execute { client.eraseText(charactersToErase, appIds) }
}

override fun shake(){
execute { client.shake() }
}

private fun activeAppId(): String {
return execute {
val appIds = getInstalledApps()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,21 @@ data class ToggleAirplaneModeCommand(
}
}

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

override fun description(): String {
return label ?: "Shakes the device"
}

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


internal fun tapOnDescription(isLongPress: Boolean?, repeat: TapRepeat?): String {
return if (isLongPress == true) "Long press"
else if (repeat != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ data class MaestroCommand(
val setAirplaneModeCommand: SetAirplaneModeCommand? = null,
val toggleAirplaneModeCommand: ToggleAirplaneModeCommand? = null,
val retryCommand: RetryCommand? = null,
val shakeCommand: ShakeCommand? = null,
) {

constructor(command: Command) : this(
Expand Down Expand Up @@ -112,7 +113,8 @@ data class MaestroCommand(
addMediaCommand = command as? AddMediaCommand,
setAirplaneModeCommand = command as? SetAirplaneModeCommand,
toggleAirplaneModeCommand = command as? ToggleAirplaneModeCommand,
retryCommand = command as? RetryCommand
retryCommand = command as? RetryCommand,
shakeCommand = command as? ShakeCommand
)

fun asCommand(): Command? = when {
Expand Down Expand Up @@ -157,6 +159,7 @@ data class MaestroCommand(
setAirplaneModeCommand != null -> setAirplaneModeCommand
toggleAirplaneModeCommand != null -> toggleAirplaneModeCommand
retryCommand != null -> retryCommand
shakeCommand != null -> shakeCommand
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ class Orchestra(
is SetAirplaneModeCommand -> setAirplaneMode(command)
is ToggleAirplaneModeCommand -> toggleAirplaneMode()
is RetryCommand -> retryCommand(command, config)
is ShakeCommand -> shakeCommand(command)
else -> true
}.also { mutating ->
if (mutating) {
Expand Down Expand Up @@ -497,6 +498,12 @@ class Orchestra(
return true
}

private fun shakeCommand(command: ShakeCommand): Boolean {
maestro.shake()

return true
}

private fun scrollVerticalCommand(): Boolean {
maestro.scrollVertical()
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import maestro.orchestra.TapOnPointV2Command
import maestro.orchestra.ToggleAirplaneModeCommand
import maestro.orchestra.TravelCommand
import maestro.orchestra.WaitForAnimationToEndCommand
import maestro.orchestra.ShakeCommand
import maestro.orchestra.error.InvalidFlowFile
import maestro.orchestra.error.MediaFileNotFound
import maestro.orchestra.error.SyntaxError
Expand Down Expand Up @@ -123,6 +124,7 @@ data class YamlFluentCommand(
val setAirplaneMode: YamlSetAirplaneMode? = null,
val toggleAirplaneMode: YamlToggleAirplaneMode? = null,
val retry: YamlRetryCommand? = null,
val shake: YamlShake? = null,
) {

@SuppressWarnings("ComplexMethod")
Expand Down Expand Up @@ -350,6 +352,12 @@ data class YamlFluentCommand(
)
)

shake != null -> listOf(
MaestroCommand(
ShakeCommand()
)
)

clearState != null -> listOf(
MaestroCommand(
ClearStateCommand(
Expand Down Expand Up @@ -1028,6 +1036,10 @@ data class YamlFluentCommand(
assertNoDefectsWithAI = YamlAssertNoDefectsWithAI()
)

"shake" -> YamlFluentCommand(
shake = YamlShake()
)

else -> throw SyntaxError("Invalid command: \"$stringCommand\"")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator

data class YamlShake(
val label: String? = null,
val optional: Boolean = false,
) {

companion object {

@JvmStatic
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
fun parse() = YamlShake()

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import maestro.orchestra.TapOnPointV2Command
import maestro.orchestra.ToggleAirplaneModeCommand
import maestro.orchestra.TravelCommand
import maestro.orchestra.WaitForAnimationToEndCommand
import maestro.orchestra.ShakeCommand
import maestro.orchestra.error.SyntaxError
import maestro.orchestra.yaml.junit.YamlCommandsExtension
import maestro.orchestra.yaml.junit.YamlExceptionExtension
Expand Down Expand Up @@ -653,4 +654,16 @@ internal class YamlCommandReaderTest {
) {
assertThat(e.message).contains("Cannot deserialize value of type")
}

@Test
fun killApp(
@YamlFile("028_shake.yaml") commands: List<Command>,
) {
assertThat(commands).containsExactly(
ApplyConfigurationCommand(MaestroConfig(
appId = "com.example.app"
)),
ShakeCommand(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
appId: com.example.app
---
- shake
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ class FakeDriver : Driver {
events += Event.SwipeElementWithDirection(elementPoint, direction, durationMs)
}

override fun shake() {
ensureOpen()

events.add(Event.Shake)
}

override fun backPress() {
ensureOpen()

Expand Down Expand Up @@ -407,6 +413,9 @@ class FakeDriver : Driver {

object HideKeyboard : Event(), UserInteraction

object Shake : Event(), UserInteraction


data class InputText(
val text: String
) : Event(), UserInteraction
Expand Down
26 changes: 26 additions & 0 deletions maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt.rej
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt (rejected hunks)
@@ -3279,6 +3279,24 @@ class IntegrationTest {
assertThat(action).isEqualTo(targetAction)
}

+ @Test
+ fun `Case 122 - Kill app`() {
+ // Given
+ val commands = readCommands("122_shake")
+
+ val driver = driver {
+ }
+
+ // When
+ Maestro(driver).use {
+ orchestra(it).runFlow(commands)
+ }
+
+ // Then
+ // No test failure
+ driver.assertHasEvent(Event.Shake("com.example.app"))
+ }
+
private fun orchestra(
maestro: Maestro,
) = Orchestra(
3 changes: 3 additions & 0 deletions maestro-test/src/test/resources/122_shake.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
appId: com.example.app
---
- shake

0 comments on commit 9050e69

Please sign in to comment.