From 501038886a9b4a089e31230a53f92296f0d4ba63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Tue, 3 Mar 2026 21:59:14 +0200 Subject: [PATCH 1/4] feat: add 6 tvOS button values for `mobile: pressButton` --- .../Categories/XCUIDevice+FBHelpers.m | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 660885630..71a45aaf4 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -268,6 +268,43 @@ - (BOOL)fb_pressButton:(NSString *)buttonName } [supportedButtonNames addObject:@"select"]; + // since tvOS 14.3 + if ([buttonName.lowercaseString isEqualToString:@"pageup"]) { + remoteButton = XCUIRemoteButtonPageUp; + } + [supportedButtonNames addObject:@"pageUp"]; + + // since tvOS 14.3 + if ([buttonName.lowercaseString isEqualToString:@"pagedown"]) { + remoteButton = XCUIRemoteButtonPageDown; + } + [supportedButtonNames addObject:@"pageDown"]; + + // since tvOS 14.3 + if ([buttonName.lowercaseString isEqualToString:@"guide"]) { + remoteButton = XCUIRemoteButtonGuide; + } + [supportedButtonNames addObject:@"guide"]; + + #if __clang_major__ >= 17 || (__clang_major__ == 16 && __clang_minor__ >= 3) + if (@available(tvOS 18.1, *)) { + if ([buttonName.lowercaseString isEqualToString:@"fourcolors"]) { + remoteButton = XCUIRemoteButtonFourColors; + } + [supportedButtonNames addObject:@"fourColors"]; + + if ([buttonName.lowercaseString isEqualToString:@"onetwothree"]) { + remoteButton = XCUIRemoteButtonOneTwoThree; + } + [supportedButtonNames addObject:@"oneTwoThree"]; + + if ([buttonName.lowercaseString isEqualToString:@"tvprovider"]) { + remoteButton = XCUIRemoteButtonTVProvider; + } + [supportedButtonNames addObject:@"tvProvider"]; + } + #endif + if (remoteButton == -1) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames] From d71ebe6ec0eb4a34d3772ebe9820c15baca3c787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Thu, 5 Mar 2026 09:21:42 +0200 Subject: [PATCH 2/4] use availableButtonNames singleton --- .../Categories/XCUIDevice+FBHelpers.m | 163 ++++++------------ 1 file changed, 56 insertions(+), 107 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 0c7eeac41..150e839c7 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -26,28 +26,63 @@ static const NSTimeInterval FBHomeButtonCoolOffTime = 1.; static const NSTimeInterval FBScreenLockTimeout = 5.; +#if TARGET_OS_TV NSDictionary *availableButtonNames(void) { static dispatch_once_t onceToken; static NSDictionary *result; dispatch_once(&onceToken, ^{ NSMutableDictionary *buttons = [NSMutableDictionary dictionary]; - - // Home button is always available - buttons[@"home"] = @(XCUIDeviceButtonHome); - -#if !TARGET_OS_TV + // https://developer.apple.com/design/human-interface-guidelines/remotes + buttons[@"up"] = @(XCUIRemoteButtonUp); // 0 + buttons[@"down"] = @(XCUIRemoteButtonDown); // 1 + buttons[@"left"] = @(XCUIRemoteButtonLeft); // 2 + buttons[@"right"] = @(XCUIRemoteButtonRight); // 3 + buttons[@"select"] = @(XCUIRemoteButtonSelect); // 4 + buttons[@"menu"] = @(XCUIRemoteButtonMenu); // 5 + buttons[@"playpause"] = @(XCUIRemoteButtonPlayPause); // 6 + buttons[@"home"] = @(XCUIRemoteButtonHome); // 7 +#if defined(XCUIRemoteButtonPageUp) // Xcode 15.3+ + buttons[@"pageup"] = @(XCUIRemoteButtonPageUp); // 9 +#endif +#if defined(XCUIRemoteButtonPageDown) // Xcode 15.3+ + buttons[@"pagedown"] = @(XCUIRemoteButtonPageDown); // 10 +#endif +#if defined(XCUIRemoteButtonGuide) // Xcode 15.3+ + buttons[@"guide"] = @(XCUIRemoteButtonGuide); // 11 +#endif + if (@available(tvOS 18.1, *)) { +#if defined(XCUIRemoteButtonFourColors) // likely Xcode 16.3+ + buttons[@"fourcolors"] = @(XCUIRemoteButtonFourColors); // 12 +#endif +#if defined(XCUIRemoteButtonOneTwoThree) // likely Xcode 16.3+ + buttons[@"onetwothree"] = @(XCUIRemoteButtonOneTwoThree); // 13 +#endif +#if defined(XCUIRemoteButtonTVProvider) // likely Xcode 16.3+ + buttons[@"tvprovider"] = @(XCUIRemoteButtonTVProvider); // 14 +#endif + } + result = [buttons copy]; + }); + return result; +} +#else +NSDictionary *availableButtonNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *buttons = [NSMutableDictionary dictionary]; + buttons[@"home"] = @(XCUIDeviceButtonHome); // 1 #if !TARGET_OS_SIMULATOR - buttons[@"volumeup"] = @(XCUIDeviceButtonVolumeUp); - buttons[@"volumedown"] = @(XCUIDeviceButtonVolumeDown); + buttons[@"volumeup"] = @(XCUIDeviceButtonVolumeUp); // 2 + buttons[@"volumedown"] = @(XCUIDeviceButtonVolumeDown); // 3 #endif - if (@available(iOS 16.0, *)) { -#if defined(XCUIDeviceButtonAction) +#if defined(XCUIDeviceButtonAction) // likely Xcode 15+ if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonAction]) { - buttons[@"action"] = @(XCUIDeviceButtonAction); + buttons[@"action"] = @(XCUIDeviceButtonAction); // 4 } #endif -#if defined(XCUIDeviceButtonCamera) +#if defined(XCUIDeviceButtonCamera) // likely Xcode 16+ #if !TARGET_OS_SIMULATOR if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonCamera]) { buttons[@"camera"] = @(XCUIDeviceButtonCamera); @@ -55,11 +90,11 @@ #endif #endif } -#endif result = [buttons copy]; }); return result; } +#endif @implementation XCUIDevice (FBHelpers) @@ -257,108 +292,22 @@ - (BOOL)fb_pressButton:(NSString *)buttonName #if !TARGET_OS_TV return [self fb_pressButton:buttonName error:error]; #else - NSMutableArray *supportedButtonNames = [NSMutableArray array]; - NSInteger remoteButton = -1; // no remote button - if ([buttonName.lowercaseString isEqualToString:@"home"]) { - // XCUIRemoteButtonHome = 7 - remoteButton = XCUIRemoteButtonHome; - } - [supportedButtonNames addObject:@"home"]; - // https://developer.apple.com/design/human-interface-guidelines/tvos/remote-and-controllers/remote/ - if ([buttonName.lowercaseString isEqualToString:@"up"]) { - // XCUIRemoteButtonUp = 0, - remoteButton = XCUIRemoteButtonUp; - } - [supportedButtonNames addObject:@"up"]; - - if ([buttonName.lowercaseString isEqualToString:@"down"]) { - // XCUIRemoteButtonDown = 1, - remoteButton = XCUIRemoteButtonDown; - } - [supportedButtonNames addObject:@"down"]; - - if ([buttonName.lowercaseString isEqualToString:@"left"]) { - // XCUIRemoteButtonLeft = 2, - remoteButton = XCUIRemoteButtonLeft; - } - [supportedButtonNames addObject:@"left"]; - - if ([buttonName.lowercaseString isEqualToString:@"right"]) { - // XCUIRemoteButtonRight = 3, - remoteButton = XCUIRemoteButtonRight; - } - [supportedButtonNames addObject:@"right"]; - - if ([buttonName.lowercaseString isEqualToString:@"menu"]) { - // XCUIRemoteButtonMenu = 5, - remoteButton = XCUIRemoteButtonMenu; - } - [supportedButtonNames addObject:@"menu"]; - - if ([buttonName.lowercaseString isEqualToString:@"playpause"]) { - // XCUIRemoteButtonPlayPause = 6, - remoteButton = XCUIRemoteButtonPlayPause; - } - [supportedButtonNames addObject:@"playpause"]; - - if ([buttonName.lowercaseString isEqualToString:@"select"]) { - // XCUIRemoteButtonSelect = 4, - remoteButton = XCUIRemoteButtonSelect; - } - [supportedButtonNames addObject:@"select"]; - - // since tvOS 14.3 - if ([buttonName.lowercaseString isEqualToString:@"pageup"]) { - remoteButton = XCUIRemoteButtonPageUp; - } - [supportedButtonNames addObject:@"pageUp"]; - - // since tvOS 14.3 - if ([buttonName.lowercaseString isEqualToString:@"pagedown"]) { - remoteButton = XCUIRemoteButtonPageDown; - } - [supportedButtonNames addObject:@"pageDown"]; - - // since tvOS 14.3 - if ([buttonName.lowercaseString isEqualToString:@"guide"]) { - remoteButton = XCUIRemoteButtonGuide; - } - [supportedButtonNames addObject:@"guide"]; - - #if __clang_major__ >= 17 || (__clang_major__ == 16 && __clang_minor__ >= 3) - if (@available(tvOS 18.1, *)) { - if ([buttonName.lowercaseString isEqualToString:@"fourcolors"]) { - remoteButton = XCUIRemoteButtonFourColors; - } - [supportedButtonNames addObject:@"fourColors"]; - - if ([buttonName.lowercaseString isEqualToString:@"onetwothree"]) { - remoteButton = XCUIRemoteButtonOneTwoThree; - } - [supportedButtonNames addObject:@"oneTwoThree"]; - - if ([buttonName.lowercaseString isEqualToString:@"tvprovider"]) { - remoteButton = XCUIRemoteButtonTVProvider; - } - [supportedButtonNames addObject:@"tvProvider"]; - } - #endif - - if (remoteButton == -1) { + NSDictionary *availableButtons = availableButtonNames(); + NSNumber *buttonValue = availableButtons[buttonName.lowercaseString]; + + if (!buttonValue) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, supportedButtonNames] + withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, availableButtons.allKeys] buildError:error]; } - if (duration) { - // https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton - [[XCUIRemote sharedRemote] pressButton:remoteButton forDuration:duration.doubleValue]; + // https://developer.apple.com/documentation/xcuiautomation/xcuiremote/press(_:forduration:) + [[XCUIRemote sharedRemote] pressButton:(XCUIRemoteButton)[buttonValue unsignedIntegerValue] forDuration:duration.doubleValue]; } else { - // https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton - [[XCUIRemote sharedRemote] pressButton:remoteButton]; + // https://developer.apple.com/documentation/xcuiautomation/xcuiremote/press(_:) + [[XCUIRemote sharedRemote] pressButton:(XCUIRemoteButton)[buttonValue unsignedIntegerValue]]; } - return YES; #endif } From cec97b8162b312d633c2f6de74f2cd9b411ce60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Thu, 5 Mar 2026 09:52:50 +0200 Subject: [PATCH 3/4] use clang compile guards --- .../Categories/XCUIDevice+FBHelpers.m | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 150e839c7..2200109ca 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -41,26 +41,18 @@ buttons[@"menu"] = @(XCUIRemoteButtonMenu); // 5 buttons[@"playpause"] = @(XCUIRemoteButtonPlayPause); // 6 buttons[@"home"] = @(XCUIRemoteButtonHome); // 7 -#if defined(XCUIRemoteButtonPageUp) // Xcode 15.3+ +#if __clang_major__ >= 15 // Xcode 15+ buttons[@"pageup"] = @(XCUIRemoteButtonPageUp); // 9 -#endif -#if defined(XCUIRemoteButtonPageDown) // Xcode 15.3+ buttons[@"pagedown"] = @(XCUIRemoteButtonPageDown); // 10 -#endif -#if defined(XCUIRemoteButtonGuide) // Xcode 15.3+ buttons[@"guide"] = @(XCUIRemoteButtonGuide); // 11 #endif +#if __clang_major__ >= 17 // likely Xcode 16.3+ if (@available(tvOS 18.1, *)) { -#if defined(XCUIRemoteButtonFourColors) // likely Xcode 16.3+ buttons[@"fourcolors"] = @(XCUIRemoteButtonFourColors); // 12 -#endif -#if defined(XCUIRemoteButtonOneTwoThree) // likely Xcode 16.3+ buttons[@"onetwothree"] = @(XCUIRemoteButtonOneTwoThree); // 13 -#endif -#if defined(XCUIRemoteButtonTVProvider) // likely Xcode 16.3+ buttons[@"tvprovider"] = @(XCUIRemoteButtonTVProvider); // 14 -#endif } +#endif result = [buttons copy]; }); return result; @@ -77,17 +69,15 @@ buttons[@"volumedown"] = @(XCUIDeviceButtonVolumeDown); // 3 #endif if (@available(iOS 16.0, *)) { -#if defined(XCUIDeviceButtonAction) // likely Xcode 15+ +#if __clang_major__ >= 15 // likely Xcode 15+ if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonAction]) { buttons[@"action"] = @(XCUIDeviceButtonAction); // 4 } #endif -#if defined(XCUIDeviceButtonCamera) // likely Xcode 16+ -#if !TARGET_OS_SIMULATOR +#if (!TARGET_OS_SIMULATOR && __clang_major__ >= 16) // likely Xcode 16+ if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonCamera]) { buttons[@"camera"] = @(XCUIDeviceButtonCamera); } -#endif #endif } result = [buttons copy]; From 95226792bc331843aef12993507cdc8092b42a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Thu, 5 Mar 2026 20:54:24 +0200 Subject: [PATCH 4/4] address comments --- .../Categories/XCUIDevice+FBHelpers.m | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 2200109ca..7835a1403 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -27,7 +27,7 @@ static const NSTimeInterval FBScreenLockTimeout = 5.; #if TARGET_OS_TV -NSDictionary *availableButtonNames(void) { +NSDictionary *fb_availableButtonNames(void) { static dispatch_once_t onceToken; static NSDictionary *result; dispatch_once(&onceToken, ^{ @@ -58,7 +58,7 @@ return result; } #else -NSDictionary *availableButtonNames(void) { +NSDictionary *fb_availableButtonNames(void) { static dispatch_once_t onceToken; static NSDictionary *result; dispatch_once(&onceToken, ^{ @@ -272,7 +272,7 @@ - (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError - (BOOL)fb_hasButton:(NSString *)buttonName { - return availableButtonNames()[buttonName.lowercaseString] != nil; + return fb_availableButtonNames()[buttonName.lowercaseString] != nil; } - (BOOL)fb_pressButton:(NSString *)buttonName @@ -283,12 +283,13 @@ - (BOOL)fb_pressButton:(NSString *)buttonName return [self fb_pressButton:buttonName error:error]; #else - NSDictionary *availableButtons = availableButtonNames(); + NSDictionary *availableButtons = fb_availableButtonNames(); NSNumber *buttonValue = availableButtons[buttonName.lowercaseString]; if (!buttonValue) { + NSArray *sortedKeys = [availableButtons.allKeys sortedArrayUsingSelector:@selector(compare:)]; return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, availableButtons.allKeys] + withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys] buildError:error]; } if (duration) { @@ -306,12 +307,13 @@ - (BOOL)fb_pressButton:(NSString *)buttonName - (BOOL)fb_pressButton:(NSString *)buttonName error:(NSError **)error { - NSDictionary *availableButtons = availableButtonNames(); + NSDictionary *availableButtons = fb_availableButtonNames(); NSNumber *buttonValue = availableButtons[buttonName.lowercaseString]; if (!buttonValue) { + NSArray *sortedKeys = [availableButtons.allKeys sortedArrayUsingSelector:@selector(compare:)]; return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, availableButtons.allKeys] + withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys] buildError:error]; } [self pressButton:(XCUIDeviceButton)[buttonValue unsignedIntegerValue]];