Skip to content

Commit

Permalink
Improved web support (#2156)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-zaitsev authored Dec 14, 2024
1 parent 3c2e1dc commit ada536a
Show file tree
Hide file tree
Showing 22 changed files with 441 additions and 73 deletions.
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ micrometerCore = "1.13.4"
mockk = "1.12.0"
mozillaRhino = "1.7.14"
picocli = "4.6.3"
selenium = "4.13.0"
selenium = "4.26.0"
selenium-devtools = "4.26.0"
skiko = "0.8.18"
slf4j = "1.7.36"
squareOkhttp = "4.12.0"
Expand Down Expand Up @@ -114,6 +115,7 @@ mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "mozillaRhino" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" }
selenium = { module = "org.seleniumhq.selenium:selenium-java", version.ref = "selenium" }
selenium-devtools = { module = "org.seleniumhq.selenium:selenium-devtools-v130", version.ref = "selenium-devtools" }
skiko-macos-arm64 = { module = "org.jetbrains.skiko:skiko-awt-runtime-macos-arm64", version.ref = "skiko" }
skiko-macos-x64 = { module = "org.jetbrains.skiko:skiko-awt-runtime-macos-x64", version.ref = "skiko" }
skiko-linux-arm64 = { module = "org.jetbrains.skiko:skiko-awt-runtime-linux-arm64", version.ref = "skiko" }
Expand Down
9 changes: 9 additions & 0 deletions installLocally.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh

./gradlew :maestro-cli:installDist

rm -rf ~/.maestro/bin
rm -rf ~/.maestro/lib

cp -r ./maestro-cli/build/install/maestro/bin ~/.maestro/bin
cp -r ./maestro-cli/build/install/maestro/lib ~/.maestro/lib
26 changes: 14 additions & 12 deletions maestro-cli/src/main/java/maestro/cli/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package maestro.cli

import maestro.MaestroException
import maestro.cli.analytics.Analytics
import maestro.cli.command.BugReportCommand
import maestro.cli.command.CloudCommand
Expand All @@ -33,16 +34,16 @@ import maestro.cli.command.StudioCommand
import maestro.cli.command.TestCommand
import maestro.cli.command.UploadCommand
import maestro.cli.update.Updates
import maestro.cli.util.ChangeLogUtils
import maestro.cli.util.ErrorReporter
import maestro.cli.view.box
import maestro.debuglog.DebugLogStore
import picocli.AutoComplete.GenerateCompletion
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.util.Properties
import java.util.*
import kotlin.system.exitProcess
import maestro.cli.util.ChangeLogUtils

@Command(
name = "maestro",
Expand Down Expand Up @@ -127,8 +128,7 @@ fun main(args: Array<String>) {
cmd.colorScheme.errorText(ex.message)
)

// Print stack trace
if (ex !is CliError) {
if (ex !is CliError && ex !is MaestroException.UnsupportedJavaVersion) {
cmd.err.println("\nThe stack trace was:")
cmd.err.println(ex.stackTraceToString())
}
Expand All @@ -150,14 +150,16 @@ fun main(args: Array<String>) {
System.err.println()
val changelog = Updates.getChangelog()
val anchor = newVersion.toString().replace(".", "")
System.err.println(listOf(
"A new version of the Maestro CLI is available ($newVersion).\n",
"See what's new:",
"https://github.com/mobile-dev-inc/maestro/blob/main/CHANGELOG.md#$anchor",
ChangeLogUtils.print(changelog),
"Upgrade command:",
"curl -Ls \"https://get.maestro.mobile.dev\" | bash",
).joinToString("\n").box())
System.err.println(
listOf(
"A new version of the Maestro CLI is available ($newVersion).\n",
"See what's new:",
"https://github.com/mobile-dev-inc/maestro/blob/main/CHANGELOG.md#$anchor",
ChangeLogUtils.print(changelog),
"Upgrade command:",
"curl -Ls \"https://get.maestro.mobile.dev\" | bash",
).joinToString("\n").box()
)
}

if (commandLine.isVersionHelpRequested) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import maestro.cli.report.TestDebugReporter
import maestro.cli.runner.TestRunner
import maestro.cli.runner.resultview.AnsiResultView
import maestro.cli.session.MaestroSessionManager
import maestro.cli.util.FileUtils.isWebFlow
import okio.sink
import picocli.CommandLine
import picocli.CommandLine.Option
Expand Down Expand Up @@ -98,11 +99,17 @@ class RecordCommand : Callable<Int> {
TestDebugReporter.install(debugOutputPathAsString = debugOutput, printToConsole = parent?.verbose == true)
val path = TestDebugReporter.getDebugOutputPath()

val deviceId = if (flowFile.isWebFlow()) {
throw CliError("'record' command does not support web flows yet.")
} else {
parent?.deviceId
}

return MaestroSessionManager.newSession(
host = parent?.host,
port = parent?.port,
driverHostPort = parent?.port,
deviceId = parent?.deviceId,
deviceId = deviceId,
platform = parent?.platform,
) { session ->
val maestro = session.maestro
Expand Down
10 changes: 5 additions & 5 deletions maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ import maestro.cli.runner.TestSuiteInteractor
import maestro.cli.runner.resultview.AnsiResultView
import maestro.cli.runner.resultview.PlainTextResultView
import maestro.cli.session.MaestroSessionManager
import maestro.cli.util.FileUtils.isWebFlow
import maestro.cli.util.PrintUtils
import maestro.cli.view.box
import maestro.orchestra.error.ValidationError
import maestro.orchestra.util.Env.withDefaultEnvVars
import maestro.orchestra.util.Env.withInjectedShellEnvVars
import maestro.orchestra.workspace.WorkspaceExecutionPlanner
import maestro.orchestra.workspace.WorkspaceExecutionPlanner.ExecutionPlan
import maestro.orchestra.yaml.YamlCommandReader
import maestro.utils.isSingleFile
import okio.sink
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -158,8 +158,7 @@ class TestCommand : Callable<Int> {

private fun isWebFlow(): Boolean {
if (flowFiles.isSingleFile) {
val config = YamlCommandReader.readConfig(flowFiles.first().toPath())
return Regex("http(s?)://").containsMatchIn(config.appId)
return flowFiles.first().isWebFlow()
}

return false
Expand Down Expand Up @@ -210,7 +209,8 @@ class TestCommand : Callable<Int> {

val onlySequenceFlows = plan.sequence.flows.isNotEmpty() && plan.flowsToRun.isEmpty() // An edge case

val availableDevices = DeviceService.listConnectedDevices().map { it.instanceId }.toSet()
val availableDevices =
DeviceService.listConnectedDevices(includeWeb = isWebFlow()).map { it.instanceId }.toSet()
val deviceIds = getPassedOptionsDeviceIds()
.filter { device ->
if (device !in availableDevices) {
Expand Down Expand Up @@ -413,7 +413,7 @@ class TestCommand : Callable<Int> {

private fun getPassedOptionsDeviceIds(): List<String> {
val arguments = if (isWebFlow()) {
PrintUtils.warn("Web support is an experimental feature and may be removed in future versions.\n")
PrintUtils.warn("Web support is in Beta. We would appreciate your feedback!\n")
"chromium"
} else parent?.deviceId
val deviceIds = arguments
Expand Down
43 changes: 29 additions & 14 deletions maestro-cli/src/main/java/maestro/cli/device/DeviceService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ import util.LocalSimulatorUtils
import util.LocalSimulatorUtils.SimctlError
import util.SimctlList
import java.io.File
import java.util.UUID
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

object DeviceService {
private val logger = LoggerFactory.getLogger(DeviceService::class.java)
fun startDevice(device: Device.AvailableForLaunch, driverHostPort: Int?, connectedDevices: Set<String> = setOf()): Device.Connected {
fun startDevice(
device: Device.AvailableForLaunch,
driverHostPort: Int?,
connectedDevices: Set<String> = setOf()
): Device.Connected {
when (device.platform) {
Platform.IOS -> {
try {
Expand Down Expand Up @@ -113,31 +117,35 @@ object DeviceService {
}
}

fun listConnectedDevices(): List<Device.Connected> {
return listDevices()
fun listConnectedDevices(includeWeb: Boolean = false): List<Device.Connected> {
return listDevices(includeWeb = includeWeb)
.filterIsInstance<Device.Connected>()
}

fun <T : Device> List<T>.withPlatform(platform: Platform?) =
filter { platform == null || it.platform == platform }

fun listAvailableForLaunchDevices(): List<Device.AvailableForLaunch> {
return listDevices()
fun listAvailableForLaunchDevices(includeWeb: Boolean = false): List<Device.AvailableForLaunch> {
return listDevices(includeWeb = includeWeb)
.filterIsInstance<Device.AvailableForLaunch>()
}

private fun listDevices(): List<Device> {
return listAndroidDevices() + listIOSDevices() + listWebDevices()
private fun listDevices(includeWeb: Boolean): List<Device> {
return listAndroidDevices() +
listIOSDevices() +
if (includeWeb) {
listWebDevices()
} else {
listOf()
}
}

private fun listWebDevices(): List<Device> {
return listOf(
Device.AvailableForLaunch(
Device.Connected(
platform = Platform.WEB,
description = "Chromium Desktop Browser (Experimental)",
modelId = "chromium",
language = null,
country = null,
instanceId = "chromium"
)
)
}
Expand Down Expand Up @@ -234,10 +242,17 @@ object DeviceService {
Platform.IOS -> listIOSDevices()
.filterIsInstance<Device.Connected>()
.find { it.description.contains(deviceName, ignoreCase = true) }

else -> runCatching {
(Dadb.list() + AdbServer.listDadbs(adbServerPort = 5038))
.mapNotNull { dadb -> runCatching { dadb.shell("getprop ro.kernel.qemu.avd_name").output }.getOrNull() }
.map { output -> Device.Connected(instanceId = output, description = output, platform = Platform.ANDROID) }
.map { output ->
Device.Connected(
instanceId = output,
description = output,
platform = Platform.ANDROID
)
}
.find { connectedDevice -> connectedDevice.description.contains(deviceName, ignoreCase = true) }
}.getOrNull()
}
Expand Down Expand Up @@ -316,7 +331,7 @@ object DeviceService {
shardIndex: Int? = null,
): String {
val avd = requireAvdManagerBinary()
val name = "${deviceName}${"_${(shardIndex ?: 0)+1}"}"
val name = "${deviceName}${"_${(shardIndex ?: 0) + 1}"}"
val command = mutableListOf(
avd.absolutePath,
"create", "avd",
Expand Down
6 changes: 6 additions & 0 deletions maestro-cli/src/main/java/maestro/cli/util/FileUtils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package maestro.cli.util

import maestro.orchestra.yaml.YamlCommandReader
import java.io.File
import java.util.zip.ZipInputStream

Expand All @@ -14,4 +15,9 @@ object FileUtils {
}
}

fun File.isWebFlow(): Boolean {
val config = YamlCommandReader.readConfig(toPath())
return Regex("http(s?)://").containsMatchIn(config.appId)
}

}
3 changes: 3 additions & 0 deletions maestro-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ dependencies {
api(libs.apk.parser)

implementation(project(":maestro-ios"))
implementation(project(":maestro-web"))
implementation(libs.google.findbugs)
implementation(libs.axml)
implementation(libs.selenium)
implementation(libs.selenium.devtools)
implementation(libs.jcodec)
api(libs.slf4j)
api(libs.logback) {
exclude(group = "org.slf4j", module = "slf4j-api")
Expand Down
2 changes: 2 additions & 0 deletions maestro-client/src/main/java/maestro/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ sealed class MaestroException(override val message: String) : RuntimeException(m
class DeprecatedCommand(message: String) : MaestroException(message)

class NoRootAccess(message: String) : MaestroException(message)

class UnsupportedJavaVersion(message: String) : MaestroException(message)
}

sealed class MaestroDriverStartupException(override val message: String): RuntimeException() {
Expand Down
10 changes: 0 additions & 10 deletions maestro-client/src/main/java/maestro/KeyCode.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package maestro

import org.openqa.selenium.Keys

enum class KeyCode(
val description: String,
) {
Expand Down Expand Up @@ -42,14 +40,6 @@ enum class KeyCode(
val lowercaseName = name.lowercase()
return values().find { it.description.lowercase() == lowercaseName }
}

fun mapToSeleniumKey(code: KeyCode): Keys? {
return when (code) {
ENTER -> Keys.ENTER
BACKSPACE -> Keys.BACK_SPACE
else -> null
}
}
}

}
11 changes: 11 additions & 0 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,17 @@ class Maestro(
}

fun web(isStudio: Boolean): Maestro {
// Check that JRE is at least 11
val version = System.getProperty("java.version")
if (version.startsWith("1.")) {
val majorVersion = version.substring(2, 3).toInt()
if (majorVersion < 11) {
throw MaestroException.UnsupportedJavaVersion(
"Maestro Web requires Java 11 or later. Current version: $version"
)
}
}

val driver = WebDriver(isStudio)
driver.open()
return Maestro(driver)
Expand Down
Loading

0 comments on commit ada536a

Please sign in to comment.