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

Platform specific appId #1952

Open
wants to merge 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class TestCommand : Callable<Int> {
private fun isWebFlow(): Boolean {
if (!flowFile.isDirectory) {
val config = YamlCommandReader.readConfig(flowFile.toPath())
return Regex("http(s?)://").containsMatchIn(config.appId)
return Regex("http(s?)://").containsMatchIn(config.appId.web ?: "")
}

return false
Expand Down
8 changes: 6 additions & 2 deletions maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.michaelbull.result.get
import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure
import maestro.Maestro
import maestro.Platform
import maestro.cli.device.Device
import maestro.cli.report.FlowAIOutput
import maestro.cli.report.FlowDebugOutput
Expand All @@ -23,6 +24,7 @@ import org.slf4j.LoggerFactory
import java.io.File
import java.nio.file.Path
import kotlin.concurrent.thread
import maestro.orchestra.withPlatformInAppId

/**
* Knows how to run a single Maestro flow (either one-shot or continuously).
Expand Down Expand Up @@ -51,13 +53,13 @@ object TestRunner {
)

val result = runCatching(resultView, maestro) {
val platform = device?.platform?.name?.let { Platform.fromString(it) }
val commands = YamlCommandReader.readCommands(flowFile.toPath())
.withEnv(env)

.withPlatformInAppId(platform)
YamlCommandReader.getConfig(commands)?.name?.let {
aiOutput = aiOutput.copy(flowName = it)
}

MaestroCommandRunner.runCommands(
maestro = maestro,
device = device,
Expand Down Expand Up @@ -106,9 +108,11 @@ object TestRunner {
join()
}

val platform = device?.platform?.name?.let { Platform.fromString(it) }
val commands = YamlCommandReader
.readCommands(flowFile.toPath())
.withEnv(env)
.withPlatformInAppId(platform)

// Restart the flow if anything has changed
if (commands != previousCommands) {
Expand Down
10 changes: 8 additions & 2 deletions maestro-client/src/main/java/maestro/Platform.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package maestro

enum class Platform {
ANDROID, IOS, WEB
}
ANDROID, IOS, WEB;

companion object {
fun fromString(p: String): Platform? {
return values().find { it.name.lowercase() == p.lowercase() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ data class InputTextCommand(
}

data class LaunchAppCommand(
val appId: String,
val appId: MaestroAppId,
val clearState: Boolean? = null,
val clearKeychain: Boolean? = null,
val stopApp: Boolean? = null,
Expand All @@ -427,9 +427,9 @@ data class LaunchAppCommand(
}

var result = if (clearState != true) {
"Launch app \"$appId\""
"Launch app \"${appId.description()}\""
} else {
"Launch app \"$appId\" with clear state"
"Launch app \"${appId.description()}\" with clear state"
}

if (clearKeychain == true) {
Expand Down Expand Up @@ -553,7 +553,7 @@ data class TakeScreenshotCommand(
}

data class StopAppCommand(
val appId: String,
val appId: MaestroAppId,
val label: String? = null
) : Command {

Expand All @@ -569,7 +569,7 @@ data class StopAppCommand(
}

data class KillAppCommand(
val appId: String,
val appId: MaestroAppId,
val label: String? = null
) : Command {

Expand All @@ -585,7 +585,7 @@ data class KillAppCommand(
}

data class ClearStateCommand(
val appId: String,
val appId: MaestroAppId,
val label: String? = null,
) : Command {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package maestro.orchestra

import maestro.Platform
import maestro.js.JsEngine

/**
Expand Down Expand Up @@ -164,3 +165,17 @@ data class MaestroCommand(
return asCommand()?.description() ?: "No op"
}
}

fun List<MaestroCommand>.withPlatformInAppId(platform: Platform?) = if (platform == null) this else map {
when (val c = it.asCommand()) {
is LaunchAppCommand -> MaestroCommand(c.copy(appId = c.appId.copy(platform = platform)))
is KillAppCommand -> MaestroCommand(c.copy(appId = c.appId.copy(platform = platform)))
is StopAppCommand -> MaestroCommand(c.copy(appId = c.appId.copy(platform = platform)))
is ClearStateCommand -> MaestroCommand(c.copy(appId = c.appId.copy(platform = platform)))
is RunFlowCommand ->
MaestroCommand(c.copy(config = c.config?.copy(appId = c.config.appId?.copy(platform = platform))))
is ApplyConfigurationCommand ->
MaestroCommand(c.copy(config = c.config.copy(appId = c.config.appId?.copy(platform = platform))))
else -> it
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package maestro.orchestra

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import maestro.Platform
import maestro.js.JsEngine
import maestro.orchestra.util.Env.evaluateScripts

// Note: The appId config is only a yaml concept for now. It'll be a larger migration to get to a point
// where appId is part of MaestroConfig (and factored out of MaestroCommands - eg: LaunchAppCommand).
data class MaestroConfig(
val appId: String? = null,
val appId: MaestroAppId? = null,
val name: String? = null,
val tags: List<String>? = emptyList(),
val ext: Map<String, Any?> = emptyMap(),
Expand All @@ -25,6 +31,53 @@ data class MaestroConfig(

}

@JsonDeserialize(using = MaestroAppId.Deserializer::class)
data class MaestroAppId(
val android: String?,
val ios: String?,
val web: String?,
private val platform: Platform? = null,
) {
constructor(appId: String) : this(appId, appId, appId)

fun forPlatform(platform: Platform) = when (platform) {
Platform.ANDROID -> android ?: error("No appId specified for android.")
Platform.IOS -> ios ?: error("No appId specified for ios.")
Platform.WEB -> web ?: error("No appId specified for web.")
}

fun evaluateScripts(jsEngine: JsEngine) = copy(
android = android?.evaluateScripts(jsEngine),
ios = ios?.evaluateScripts(jsEngine),
web = web?.evaluateScripts(jsEngine),
)

fun description() =
if (listOfNotNull(android, ios, web).toSet().size == 1) listOfNotNull(android, ios, web).first()
else if (platform != null) forPlatform(platform)
else listOf("android", "ios", "web")
.zip(listOf(android, ios, web))
.filter { it.second != null }
.joinToString(", ") { "${it.first}: ${it.second}" }

class Deserializer : JsonDeserializer<MaestroAppId>() {

override fun deserialize(parser: JsonParser, context: DeserializationContext): MaestroAppId {
val node: JsonNode = parser.codec.readTree(parser)
return when {
node.isTextual -> MaestroAppId(node.asText())
node.isObject -> {
val android = node.get("android")?.asText()
val ios = node.get("ios")?.asText()
val web = node.get("web")?.asText()
MaestroAppId(android, ios, web)
}
else -> throw IllegalArgumentException("Unexpected JSON format for MaestroAppId: $node")
}
}
}
}

data class MaestroOnFlowComplete(val commands: List<MaestroCommand>) {
fun evaluateScripts(jsEngine: JsEngine): MaestroOnFlowComplete {
return this
Expand Down
26 changes: 17 additions & 9 deletions maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt
Original file line number Diff line number Diff line change
Expand Up @@ -451,22 +451,25 @@ class Orchestra(
}

private fun clearAppStateCommand(command: ClearStateCommand): Boolean {
maestro.clearAppState(command.appId)
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.clearAppState(appId)
// Android's clear command also resets permissions
// Reset all permissions to unset so both platforms behave the same
maestro.setPermissions(command.appId, mapOf("all" to "unset"))
maestro.setPermissions(appId, mapOf("all" to "unset"))

return true
}

private fun stopAppCommand(command: StopAppCommand): Boolean {
maestro.stopApp(command.appId)
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.stopApp(appId)

return true
}

private fun killAppCommand(command: KillAppCommand): Boolean {
maestro.killApp(command.appId)
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.killApp(appId)

return true
}
Expand Down Expand Up @@ -766,7 +769,8 @@ class Orchestra(
}

private fun openLinkCommand(command: OpenLinkCommand, config: MaestroConfig?): Boolean {
maestro.openLink(command.link, config?.appId, command.autoVerify ?: false, command.browser ?: false)
val appId = config?.appId?.forPlatform(deviceInfo().platform)
maestro.openLink(command.link, appId, command.autoVerify ?: false, command.browser ?: false)

return true
}
Expand All @@ -777,20 +781,23 @@ class Orchestra(
maestro.clearKeychain()
}
if (command.clearState == true) {
maestro.clearAppState(command.appId)
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.clearAppState(appId)
}

// For testing convenience, default to allow all on app launch
val permissions = command.permissions ?: mapOf("all" to "allow")
maestro.setPermissions(command.appId, permissions)
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.setPermissions(appId, permissions)

} catch (e: Exception) {
throw MaestroException.UnableToClearState("Unable to clear state for app ${command.appId}")
}

try {
val appId = command.appId.forPlatform(deviceInfo().platform)
maestro.launchApp(
appId = command.appId,
appId = appId,
launchArguments = command.launchArguments ?: emptyMap(),
stopIfRunning = command.stopApp ?: true
)
Expand Down Expand Up @@ -842,13 +849,14 @@ class Orchestra(
): Boolean {
return try {
val result = findElement(command.selector)
val appId = config?.appId?.forPlatform(deviceInfo().platform)
maestro.tap(
result.element,
result.hierarchy,
retryIfNoChange,
waitUntilVisible,
command.longPress ?: false,
config?.appId,
appId,
tapRepeat = command.repeat,
waitToSettleTimeoutMs = command.waitToSettleTimeoutMs
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package maestro.orchestra.yaml

import com.fasterxml.jackson.annotation.JsonCreator
import java.lang.UnsupportedOperationException
import maestro.orchestra.MaestroAppId

data class YamlAppId(
val android: String?,
val ios: String?,
val web: String?,
){
fun asAppId() = MaestroAppId(android, ios, web)

companion object {

@JvmStatic
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
fun parse(appId: Any): YamlAppId {
val appIdString = when (appId) {
is String -> appId
is Map<*, *> -> {
val android = appId.getOrDefault("android", null) as String?
val ios = appId.getOrDefault("ios", null) as String?
val web = appId.getOrDefault("web", null) as String?
if (
android == null &&
ios == null &&
web == null
) {
throw UnsupportedOperationException("Cannot deserialize appId with no specified platform.")
}
return YamlAppId(android, ios, web)
}
else -> throw UnsupportedOperationException("Cannot deserialize appId with data type ${appId.javaClass}")
}
return YamlAppId(appIdString, appIdString, appIdString)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package maestro.orchestra.yaml
import com.fasterxml.jackson.annotation.JsonCreator

data class YamlClearState(
val appId: String? = null,
val appId: YamlAppId? = null,
val label: String? = null,
) {
companion object {
@JvmStatic
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
fun parse(appId: String) = YamlClearState(
appId = appId,
appId = YamlAppId.parse(appId),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ object YamlCommandReader {
fun readCommands(flowPath: Path): List<MaestroCommand> = mapParsingErrors(flowPath) {
val (config, commands) = readConfigAndCommands(flowPath)
val maestroCommands = commands
.flatMap { it.toCommands(flowPath, config.appId) }
.flatMap { it.toCommands(flowPath, config.appId.asAppId()) }
.withEnv(config.env)

listOfNotNull(config.toCommand(flowPath), *maestroCommands.toTypedArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.nio.file.Path

data class YamlConfig(
val name: String?,
val appId: String,
val appId: YamlAppId,
val tags: List<String>? = emptyList(),
val env: Map<String, String> = emptyMap(),
val onFlowStart: YamlOnFlowStart?,
Expand All @@ -26,7 +26,7 @@ data class YamlConfig(

fun toCommand(flowPath: Path): MaestroCommand {
val config = MaestroConfig(
appId = appId,
appId = appId.asAppId(),
name = name,
tags = tags,
ext = ext.toMap(),
Expand All @@ -39,12 +39,12 @@ data class YamlConfig(
private fun onFlowComplete(flowPath: Path): MaestroOnFlowComplete? {
if (onFlowComplete == null) return null

return MaestroOnFlowComplete(onFlowComplete.commands.flatMap { it.toCommands(flowPath, appId) })
return MaestroOnFlowComplete(onFlowComplete.commands.flatMap { it.toCommands(flowPath, appId.asAppId()) })
}

private fun onFlowStart(flowPath: Path): MaestroOnFlowStart? {
if (onFlowStart == null) return null

return MaestroOnFlowStart(onFlowStart.commands.flatMap { it.toCommands(flowPath, appId) })
return MaestroOnFlowStart(onFlowStart.commands.flatMap { it.toCommands(flowPath, appId.asAppId()) })
}
}
Loading
Loading