diff --git a/cli/agent.go b/cli/agent.go index 7242f1a..5164334 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -16,8 +16,10 @@ import ( const ( agentVersionIOS = "0.0.20" + agentVersionTVOS = "0.0.20" agentVersionAndroid = "1.2.4" iosRunnerBundleID = "com.mobilenext.devicekit-iosUITests.xctrunner" + tvosRunnerBundleID = "com.mobilenext.devicekit-tvosUITests.xctrunner" androidPackageName = "com.mobilenext.devicekit" ) @@ -26,6 +28,9 @@ var agentChecksums = map[string]string{ "devicekit-ios-Sim-arm64.zip": "8040f4918892f63d79713b5824184ac5f296c5ec9b23266c25af34777550f28c", "devicekit-ios-Sim-x86_64.zip": "78a8f2d208a22523efbaa5cb2a735557e807f877bb8ec1a1c31c886f2e425684", "devicekit-ios-runner.ipa": "f5fe88d4169c39001ed012101651c5ac00e8ab54aefb72c74455e7037c2e8205", + // tvOS simulator runner is published from the same devicekit-ios release. + // Update this checksum whenever the tvOS runner artifact is rebuilt/republished. + "devicekit-tvos-Sim-arm64.zip": "49061f17046055c7e89dbf27067a59ab0bffd9d7d14d63031d5411c5963ccb81", "devicekit.apk": "63b1111fbd3b986c7452bc7c28150b1e9c0d611b2ecd7f6917a0f50a84d0836b", } @@ -130,6 +135,13 @@ var agentInstallCmd = &cobra.Command{ default: return fmt.Errorf("unsupported device type: %s", device.DeviceType()) } + case "tvos": + switch device.DeviceType() { + case "simulator": + installErr = installAgentOnSimulator(device) + default: + return fmt.Errorf("unsupported tvOS device type: %s (only the tvOS Simulator is supported)", device.DeviceType()) + } case "android": installErr = installAgentOnAndroid(device) default: @@ -195,6 +207,8 @@ func agentPackageForPlatform(platform string) string { return androidPackageName case "ios": return iosRunnerBundleID + case "tvos": + return tvosRunnerBundleID default: return "" } @@ -206,6 +220,8 @@ func agentVersionForPlatform(platform string) string { return agentVersionAndroid case "ios": return agentVersionIOS + case "tvos": + return agentVersionTVOS default: return "" } @@ -259,8 +275,21 @@ func installAgentOnSimulator(device devices.ControllableDevice) error { arch = "arm64" } - filename := fmt.Sprintf("devicekit-ios-Sim-%s.zip", arch) - agentURL := fmt.Sprintf("https://github.com/mobile-next/devicekit-ios/releases/download/%s/%s", agentVersionIOS, filename) + var filename, version string + switch device.Platform() { + case "tvos": + // The tvOS runner is currently published for Apple Silicon (arm64) only. + if arch != "arm64" { + return fmt.Errorf("the tvOS simulator runner is only available for arm64 (Apple Silicon)") + } + filename = "devicekit-tvos-Sim-arm64.zip" + version = agentVersionTVOS + default: + filename = fmt.Sprintf("devicekit-ios-Sim-%s.zip", arch) + version = agentVersionIOS + } + + agentURL := fmt.Sprintf("https://github.com/mobile-next/devicekit-ios/releases/download/%s/%s", version, filename) tmpDir, err := os.MkdirTemp("", "mobilecli-agent-*") if err != nil { diff --git a/devices/common.go b/devices/common.go index b5a3639..631c1ba 100644 --- a/devices/common.go +++ b/devices/common.go @@ -347,7 +347,7 @@ func GetDeviceInfoList(opts DeviceListOptions) ([]DeviceInfo, error) { // get model for devices model := "" - if d.Platform() == "ios" { + if d.Platform() == "ios" || d.Platform() == "tvos" { if d.DeviceType() == "real" { if iosDevice, ok := d.(*IOSDevice); ok { model = iosDevice.ProductType diff --git a/devices/ios.go b/devices/ios.go index f1e0b3a..5a85b7a 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -39,6 +39,7 @@ const ( deviceKitAppLaunchTimeout = 5 * time.Second deviceKitBroadcastTimeout = 5 * time.Second agentRunnerBundleID = "com.mobilenext.devicekit-iosUITests.xctrunner" + agentRunnerBundleIDTVOS = "com.mobilenext.devicekit-tvosUITests.xctrunner" ) // deviceInfoCache caches device name and OS version to avoid expensive GetValues() calls diff --git a/devices/simulator.go b/devices/simulator.go index 6112a33..9fc1447 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -55,11 +55,12 @@ type SimulatorDevice struct { wdaClient *wda.WdaClient } -// parseSimulatorVersion parses iOS version from simulator runtime string +// parseSimulatorVersion parses the OS version from a simulator runtime string // e.g., "com.apple.CoreSimulator.SimRuntime.iOS-18-6" -> "18.6" +// e.g., "com.apple.CoreSimulator.SimRuntime.tvOS-26-5" -> "26.5" func parseSimulatorVersion(runtime string) string { - // Use regex to extract iOS version from runtime string - re := regexp.MustCompile(`iOS-(\d+)-(\d+)`) + // Use regex to extract the OS version from the runtime string + re := regexp.MustCompile(`(?:iOS|tvOS|watchOS|xrOS)-(\d+)-(\d+)`) matches := re.FindStringSubmatch(runtime) if len(matches) == 3 { return matches[1] + "." + matches[2] @@ -69,11 +70,38 @@ func parseSimulatorVersion(runtime string) string { return runtime } +// simulatorPlatform derives the mobilecli platform identifier from a simulator +// runtime string. tvOS simulators report "tvos"; everything else defaults to "ios". +func simulatorPlatform(runtime string) string { + if strings.Contains(runtime, "tvOS") { + return "tvos" + } + return "ios" +} + func (s SimulatorDevice) ID() string { return s.UDID } func (s SimulatorDevice) Name() string { return s.Simulator.Name } -func (s SimulatorDevice) Platform() string { return "ios" } +func (s SimulatorDevice) Platform() string { return simulatorPlatform(s.Runtime) } func (s SimulatorDevice) DeviceType() string { return "simulator" } func (s SimulatorDevice) Version() string { return parseSimulatorVersion(s.Runtime) } + +// agentRunnerBundleIDForPlatform returns the runner bundle id suffix expected +// for this simulator's platform (iOS vs tvOS). +func (s SimulatorDevice) agentRunnerBundleIDForPlatform() string { + if s.Platform() == "tvos" { + return agentRunnerBundleIDTVOS + } + return agentRunnerBundleID +} + +// agentRunnerProcessNameForPlatform returns the runner process name used to +// locate the running agent for this simulator's platform. +func (s SimulatorDevice) agentRunnerProcessNameForPlatform() string { + if s.Platform() == "tvos" { + return "devicekit-tvosUITests-Runner" + } + return "devicekit-iosUITests-Runner" +} func (s SimulatorDevice) State() string { if s.Simulator.State == "Booted" { return "online" @@ -300,7 +328,7 @@ func (s SimulatorDevice) findInstalledAgentBundleID() (string, error) { } for bundleID := range installedApps { - if strings.HasSuffix(bundleID, agentRunnerBundleID) { + if strings.HasSuffix(bundleID, s.agentRunnerBundleIDForPlatform()) { return bundleID, nil } } @@ -589,7 +617,7 @@ func (s *SimulatorDevice) Info() (*FullDeviceInfo, error) { DeviceInfo: DeviceInfo{ ID: s.UDID, Name: s.Simulator.Name, - Platform: "ios", + Platform: s.Platform(), Type: "simulator", Version: parseSimulatorVersion(s.Runtime), State: s.State(), @@ -715,7 +743,7 @@ func listAllProcesses() ([]ProcessInfo, error) { return processes, nil } -func findWdaProcessForDevice(deviceUDID string) (int, string, error) { +func findWdaProcessForDevice(deviceUDID, runnerProcessName string) (int, string, error) { processes, err := listAllProcesses() if err != nil { return 0, "", err @@ -724,7 +752,7 @@ func findWdaProcessForDevice(deviceUDID string) (int, string, error) { devicePath := fmt.Sprintf("/Library/Developer/CoreSimulator/Devices/%s", deviceUDID) for _, proc := range processes { - if strings.Contains(proc.Command, devicePath) && strings.Contains(proc.Command, "devicekit-iosUITests-Runner") { + if strings.Contains(proc.Command, devicePath) && strings.Contains(proc.Command, runnerProcessName) { return proc.PID, proc.Command, nil } } @@ -763,7 +791,7 @@ func extractEnvValue(output, envVar string) (string, error) { } func (s *SimulatorDevice) getWdaEnvPort(envVar string) (int, error) { - pid, processInfo, err := findWdaProcessForDevice(s.UDID) + pid, processInfo, err := findWdaProcessForDevice(s.UDID, s.agentRunnerProcessNameForPlatform()) if err != nil { utils.Verbose("Could not find WDA process: %v", err) return 0, err diff --git a/devices/wda/press-button.go b/devices/wda/press-button.go index 67338be..48057b0 100644 --- a/devices/wda/press-button.go +++ b/devices/wda/press-button.go @@ -8,6 +8,14 @@ func (c *WdaClient) PressButton(key string) error { "VOLUME_DOWN": "volumeDown", "HOME": "home", "LOCK": "lock", + // tvOS Siri Remote buttons (handled by the tvOS device.io.button handler). + "UP": "up", + "DOWN": "down", + "LEFT": "left", + "RIGHT": "right", + "SELECT": "select", + "MENU": "menu", + "PLAY_PAUSE": "playPause", } if key == "ENTER" {