diff --git a/cli/agent.go b/cli/agent.go index 998291c4..4220f1a7 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -16,8 +16,10 @@ import ( const ( agentVersionIOS = "0.0.24" + agentVersionTVOS = "0.0.20" agentVersionAndroid = "1.2.6" 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": "72b11809b7e3bb24aaa23f24458bb28bb859967b31fa9dd0a46db2fc080f6915", "devicekit-ios-Sim-x86_64.zip": "aaac190073188c48b3cbc03bcdb4536ab24db2b530187b3ea6878ade3ef821ea", "devicekit-ios-runner.ipa": "40483399f3a1a1173d8edb932ea3d836edd641b4a1dfd42f7fd7d4618db7b3f6", + // 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": "01d933a311dac113bb89f2cb3256482467c1e02b287a2fd5e412863b8f907c51", } @@ -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 50e092b9..2713e27d 100644 --- a/devices/common.go +++ b/devices/common.go @@ -348,7 +348,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/devicekit/press-button.go b/devices/devicekit/press-button.go index c1bdb274..9169faef 100644 --- a/devices/devicekit/press-button.go +++ b/devices/devicekit/press-button.go @@ -8,6 +8,14 @@ func (c *DeviceKitClient) 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" { diff --git a/devices/ios.go b/devices/ios.go index 705cc186..2a5731e9 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -40,6 +40,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 9a823d58..2f949d22 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -56,11 +56,12 @@ type SimulatorDevice struct { deviceKitClient *devicekit.DeviceKitClient } -// 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] @@ -70,11 +71,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" @@ -355,7 +383,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 } } @@ -672,7 +700,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(), @@ -798,7 +826,7 @@ func listAllProcesses() ([]ProcessInfo, error) { return processes, nil } -func findDeviceKitProcessForDevice(deviceUDID string) (int, string, error) { +func findDeviceKitProcessForDevice(deviceUDID, runnerProcessName string) (int, string, error) { processes, err := listAllProcesses() if err != nil { return 0, "", err @@ -807,7 +835,7 @@ func findDeviceKitProcessForDevice(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 } } @@ -846,7 +874,7 @@ func extractEnvValue(output, envVar string) (string, error) { } func (s *SimulatorDevice) getDeviceKitEnvPort(envVar string) (int, error) { - pid, processInfo, err := findDeviceKitProcessForDevice(s.UDID) + pid, processInfo, err := findDeviceKitProcessForDevice(s.UDID, s.agentRunnerProcessNameForPlatform()) if err != nil { utils.Verbose("Could not find WDA process: %v", err) return 0, err