Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1762de5
refactor(device-spec): reshape DeviceSpec with required fields, defau…
steviec Apr 10, 2026
a5d8008
refactor(device-spec): use enum entries for iOS/Web locale defaults
steviec Apr 10, 2026
294a287
refactor(device-spec): update DeviceService to construct DeviceSpec d…
steviec Apr 10, 2026
76280d9
refactor(device-spec): drop dead avdInfo lookup and tidy iosSpec cast…
steviec Apr 10, 2026
32361d2
refactor(device-spec): update StartDeviceCommand to construct DeviceS…
steviec Apr 11, 2026
5a7f002
refactor(device-spec): rename deviceSpec var and document fallback in…
steviec Apr 11, 2026
872183f
refactor(device-spec): update PickDeviceView to construct DeviceSpec …
steviec Apr 11, 2026
3f36da6
refactor(device-spec): update remaining maestro-cli/client call sites
steviec Apr 11, 2026
bad45f5
refactor(device-spec): update downstream modules for new DeviceSpec API
steviec Apr 11, 2026
e32a4f2
test(device-spec): rewrite DeviceSpec tests for new construction API
steviec Apr 11, 2026
124cb44
test(device-spec): add sparse serialization tests (failing until Task 9)
steviec Apr 11, 2026
b1c129a
feat(device-spec): add sparse serializer and per-subtype locale deser…
steviec Apr 11, 2026
c1a13a5
test(device-spec): update remaining tests for new DeviceSpec API
steviec Apr 11, 2026
003d6b3
refactor(device-spec): drop @get:JsonIgnore from computed properties
steviec Apr 11, 2026
9131eca
refactor(device-spec): centralize default specs via DeviceSpec.X.DEFAULT
steviec Apr 11, 2026
b79368b
refactor(device-spec): remove driver fields from DeviceSpec
steviec Apr 14, 2026
52e2310
test(device-spec): update tests for removed driver fields
steviec Apr 14, 2026
b9c537f
refactor(device-spec): promote locale, osVersion, deviceName to base …
steviec Apr 14, 2026
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 @@ -42,7 +42,6 @@ import maestro.orchestra.validation.AppValidator
import maestro.orchestra.validation.WorkspaceValidationException
import maestro.orchestra.validation.WorkspaceValidator
import maestro.device.DeviceSpec
import maestro.device.DeviceSpecRequest
import maestro.utils.TemporaryDirectory
import okio.BufferedSink
import okio.buffer
Expand Down
46 changes: 29 additions & 17 deletions maestro-cli/src/main/java/maestro/cli/command/StartDeviceCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import maestro.cli.device.DeviceCreateUtil
import maestro.device.DeviceService
import maestro.cli.report.TestDebugReporter
import maestro.cli.util.EnvUtils
import maestro.device.CPU_ARCHITECTURE
import maestro.device.DeviceSpec
import maestro.device.DeviceSpecRequest
import maestro.device.Platform
import maestro.device.locale.AndroidLocale
import maestro.device.locale.IosLocale
import maestro.device.locale.WebLocale
import picocli.CommandLine
import java.util.concurrent.Callable

Expand Down Expand Up @@ -90,30 +93,39 @@ class StartDeviceCommand : Callable<Int> {

// Get the device configuration
val parsedPlatform = Platform.fromString(platform)
val maestroDeviceConfiguration = DeviceSpec.fromRequest(
when (parsedPlatform) {
Platform.ANDROID -> DeviceSpecRequest.Android(
model = deviceModel,
os = deviceOs ?: osVersion.let { "android-$it" },
locale = deviceLocale,
val deviceSpec: DeviceSpec = when (parsedPlatform) {
Platform.ANDROID -> {
val default = DeviceSpec.Android.DEFAULT
DeviceSpec.Android(
// osVersion is nullable; ?.let prevents interpolating "android-null"
model = deviceModel ?: default.model,
os = deviceOs ?: osVersion?.let { "android-$it" } ?: default.os,
// AndroidLocale is a data class (no pre-defined constant); parse the default
locale = deviceLocale?.let { AndroidLocale.fromString(it) } ?: default.locale,
cpuArchitecture = EnvUtils.getMacOSArchitecture(),
)
Platform.IOS -> DeviceSpecRequest.Ios(
model = deviceModel,
os = deviceOs ?: osVersion.let { "iOS-$it" },
locale = deviceLocale,
}
Platform.IOS -> {
val default = DeviceSpec.Ios.DEFAULT
DeviceSpec.Ios(
model = deviceModel ?: default.model,
os = deviceOs ?: osVersion?.let { "iOS-$it" } ?: default.os,
locale = deviceLocale?.let { IosLocale.fromString(it) } ?: default.locale,
)
Platform.WEB -> DeviceSpecRequest.Web(
model = deviceModel,
os = deviceOs ?: osVersion,
locale = deviceLocale,
}
Platform.WEB -> {
val default = DeviceSpec.Web.DEFAULT
DeviceSpec.Web(
model = deviceModel ?: default.model,
os = deviceOs ?: osVersion ?: default.os,
locale = deviceLocale?.let { WebLocale.fromString(it) } ?: default.locale,
)
}
)
}

// Get/Create the device
val device = DeviceCreateUtil.getOrCreateDevice(
maestroDeviceConfiguration,
deviceSpec,
forceCreate
)

Expand Down
15 changes: 5 additions & 10 deletions maestro-cli/src/main/java/maestro/cli/device/PickDeviceView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package maestro.cli.device
import maestro.cli.CliError
import maestro.cli.util.PrintUtils
import maestro.device.Device
import maestro.device.DeviceSpecRequest
import maestro.device.DeviceSpec
import maestro.device.Platform
import org.fusesource.jansi.Ansi.ansi
Expand All @@ -30,15 +29,11 @@ object PickDeviceView {
Platform.fromString(it)
} ?: throw CliError("Please specify a platform"))

val spec = DeviceSpec.fromRequest(
when (selectedPlatform) {
Platform.ANDROID -> DeviceSpecRequest.Android()
Platform.IOS -> DeviceSpecRequest.Ios()
Platform.WEB -> DeviceSpecRequest.Web()
}
)

return spec
return when (selectedPlatform) {
Platform.ANDROID -> DeviceSpec.Android.DEFAULT
Platform.IOS -> DeviceSpec.Ios.DEFAULT
Platform.WEB -> DeviceSpec.Web.DEFAULT
}
}

fun pickRunningDevice(devices: List<Device>): Device {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import maestro.device.Device
import maestro.device.Platform
import maestro.cli.runner.CommandState
import maestro.cli.runner.CommandStatus
import maestro.device.DeviceSpecRequest
import maestro.device.DeviceSpec
import maestro.orchestra.AssertWithAICommand
import maestro.orchestra.ElementSelector
Expand Down Expand Up @@ -285,9 +284,7 @@ fun main() {
flowName = "Flow for playing around",
device = Device.Connected(
instanceId = "device",
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Android()
),
deviceSpec = DeviceSpec.Android.DEFAULT,
description = "description",
platform = Platform.ANDROID,
deviceType = Device.DeviceType.EMULATOR
Expand Down
67 changes: 33 additions & 34 deletions maestro-client/src/main/java/maestro/device/DeviceService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ object DeviceService {
Platform.IOS -> {
PrintUtils.message("Launching Simulator...")
try {
val iosSpec = device.deviceSpec as DeviceSpec.Ios
localSimulatorUtils.bootSimulator(device.modelId)
PrintUtils.message("Setting the device locale to ${device.deviceSpec.locale.code}...")
localSimulatorUtils.setDeviceLanguage(device.modelId, device.deviceSpec.locale.languageCode)
localSimulatorUtils.setDeviceLocale(device.modelId, device.deviceSpec.locale.code)
PrintUtils.message("Setting the device locale to ${iosSpec.locale.code}...")
localSimulatorUtils.setDeviceLanguage(device.modelId, iosSpec.locale.languageCode)
localSimulatorUtils.setDeviceLocale(device.modelId, iosSpec.locale.code)
localSimulatorUtils.reboot(device.modelId)
localSimulatorUtils.launchSimulator(device.modelId)
localSimulatorUtils.awaitLaunch(device.modelId)
Expand All @@ -61,6 +62,7 @@ object DeviceService {

Platform.ANDROID -> {
PrintUtils.message("Launching Emulator...")
val androidSpec = device.deviceSpec as DeviceSpec.Android
val emulatorBinary = requireEmulatorBinary()

ProcessBuilder(
Expand Down Expand Up @@ -92,19 +94,19 @@ object DeviceService {
Thread.sleep(1000)
}

PrintUtils.message("Setting the device locale to ${device.deviceSpec.locale.code}...")
PrintUtils.message("Setting the device locale to ${androidSpec.locale.code}...")
val driver = AndroidDriver(dadb, driverHostPort)
driver.installMaestroDriverApp()
val result = driver.setDeviceLocale(
country = device.deviceSpec.locale.countryCode,
language = device.deviceSpec.locale.languageCode,
country = androidSpec.locale.countryCode,
language = androidSpec.locale.languageCode,
)

when (result) {
SET_LOCALE_RESULT_SUCCESS -> PrintUtils.message("[Done] Setting the device locale to ${device.deviceSpec.locale.code}...")
SET_LOCALE_RESULT_LOCALE_NOT_VALID -> throw IllegalStateException("Failed to set locale ${device.deviceSpec.locale.code}, the locale is not valid for a chosen device")
SET_LOCALE_RESULT_UPDATE_CONFIGURATION_FAILED -> throw IllegalStateException("Failed to set locale ${device.deviceSpec.locale.code}, exception during updating configuration occurred")
else -> throw IllegalStateException("Failed to set locale ${device.deviceSpec.locale.code}, unknown exception happened")
SET_LOCALE_RESULT_SUCCESS -> PrintUtils.message("[Done] Setting the device locale to ${androidSpec.locale.code}...")
SET_LOCALE_RESULT_LOCALE_NOT_VALID -> throw IllegalStateException("Failed to set locale ${androidSpec.locale.code}, the locale is not valid for a chosen device")
SET_LOCALE_RESULT_UPDATE_CONFIGURATION_FAILED -> throw IllegalStateException("Failed to set locale ${androidSpec.locale.code}, exception during updating configuration occurred")
else -> throw IllegalStateException("Failed to set locale ${androidSpec.locale.code}, unknown exception happened")
}
driver.uninstallMaestroDriverApp()

Expand Down Expand Up @@ -166,14 +168,14 @@ object DeviceService {
description = "Chromium Web Browser",
instanceId = "chromium",
deviceType = Device.DeviceType.BROWSER,
deviceSpec = DeviceSpec.fromRequest(DeviceSpecRequest.Web())
deviceSpec = DeviceSpec.Web.DEFAULT
),
Device.AvailableForLaunch(
modelId = "chromium",
description = "Chromium Web Browser",
platform = Platform.WEB,
deviceType = Device.DeviceType.BROWSER,
deviceSpec = DeviceSpec.fromRequest(DeviceSpecRequest.Web())
deviceSpec = DeviceSpec.Web.DEFAULT
)
)
}
Expand All @@ -188,9 +190,7 @@ object DeviceService {
description = dadb.toString(),
platform = Platform.ANDROID,
deviceType = Device.DeviceType.EMULATOR,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Android()
)
deviceSpec = DeviceSpec.Android.DEFAULT
)
)
}
Expand Down Expand Up @@ -221,15 +221,12 @@ object DeviceService {
instanceId.startsWith("emulator") -> Device.DeviceType.EMULATOR
else -> Device.DeviceType.REAL
}
val avdInfo = avdInfoList.find { it.name == avdName } ?: AvdInfo(name = avdName ?: "", model = "", os = "")
Device.Connected(
instanceId = instanceId,
description = avdName ?: dadb.toString(),
platform = Platform.ANDROID,
deviceType = deviceType,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Android()
),
deviceSpec = DeviceSpec.Android.DEFAULT,
)
}
}.getOrNull() ?: emptyList()
Expand All @@ -251,9 +248,11 @@ object DeviceService {
description = avdName,
platform = Platform.ANDROID,
deviceType = Device.DeviceType.EMULATOR,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Android(avdInfo.model, avdInfo.os)
)
deviceSpec = if (avdInfo.model.isBlank() || avdInfo.os.isBlank()) {
DeviceSpec.Android.DEFAULT
} else {
DeviceSpec.Android(model = avdInfo.model, os = avdInfo.os)
}
)
}
.toList()
Expand Down Expand Up @@ -369,9 +368,7 @@ object DeviceService {
description = description,
platform = Platform.IOS,
deviceType = Device.DeviceType.REAL,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Ios()
)
deviceSpec = DeviceSpec.Ios.DEFAULT
)
}
}
Expand All @@ -395,19 +392,23 @@ object DeviceService {
description = description,
platform = Platform.IOS,
deviceType = Device.DeviceType.SIMULATOR,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Ios(model, os)
)
deviceSpec = if (model.isBlank() || os.isBlank()) {
DeviceSpec.Ios.DEFAULT
} else {
DeviceSpec.Ios(model = model, os = os)
}
)
} else {
Device.AvailableForLaunch(
modelId = device.udid,
description = description,
platform = Platform.IOS,
deviceType = Device.DeviceType.SIMULATOR,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Ios(model, os)
)
deviceSpec = if (model.isBlank() || os.isBlank()) {
DeviceSpec.Ios.DEFAULT
} else {
DeviceSpec.Ios(model = model, os = os)
}
)
}
}
Expand All @@ -430,9 +431,7 @@ object DeviceService {
description = output,
platform = Platform.ANDROID,
deviceType = Device.DeviceType.EMULATOR,
deviceSpec = DeviceSpec.fromRequest(
DeviceSpecRequest.Android()
)
deviceSpec = DeviceSpec.Android.DEFAULT
)
}
.find { connectedDevice -> connectedDevice.description.contains(deviceName, ignoreCase = true) }
Expand Down
Loading
Loading