Skip to content

Adds shake command #2259

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

have you tested this?

I've seen something like this on a SO thread:

adb emu sensor set acceleration 100:100:100; sleep 1; adb emu sensor set acceleration 0:0:0

curious how we make sure this sort of function actually works

}

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")
Copy link
Contributor

Choose a reason for hiding this comment

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

The catch block below will re-throw this with this message wrapped in the same message again:
"Failed to execute shake gesture on simulator: Failed to execute shake gesture on simulator. Exit code: $exitCode"

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the review. 68212e0

}

logger.trace("Successfully executed shake gesture")
Copy link
Contributor

Choose a reason for hiding this comment

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

Now if exitCode != 0 you will log the error, but don't do anything about it and you'll log this 'Successful' message and return normally.

Sorry I wasn't more clear in the earlier review. I'd suggest bringing the previous throw back, so the function exits properly on error. But something else needs to change to avoid the double message.

I thought of two options.

  1. cut down the message: throw IOException("Exit code: $exitCode") and the outer catch/throw will use that as the error message.
  2. Move the code that checks the exitCode outside the try block (everything from 'if' on 198 to 'logger.trace' on 203). That way so the throw IOException can throw directly to the calling function. That means the calling function will get the exception as IOException instead of XCUITestServerError.UnknownFailure.

} 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 shake(
@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
Loading