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

Adds shake command #2259

Open
wants to merge 1 commit 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
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 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