Skip to content
Merged
Changes from all commits
Commits
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
130 changes: 54 additions & 76 deletions WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,65 @@
static const NSTimeInterval FBHomeButtonCoolOffTime = 1.;
static const NSTimeInterval FBScreenLockTimeout = 5.;

NSDictionary<NSString *, NSNumber *> *availableButtonNames(void) {
#if TARGET_OS_TV
NSDictionary<NSString *, NSNumber *> *fb_availableButtonNames(void) {
static dispatch_once_t onceToken;
static NSDictionary *result;
dispatch_once(&onceToken, ^{
Comment on lines +29 to 33
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

availableButtonNames is declared as a global C function in this .m file. Since it’s only used internally, consider marking it static (or giving it an FB-prefixed name) to avoid exporting a generic symbol that could collide at link time.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it's not exported as it's not included in the headers file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment still makes sense, lets add FB prefix

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 __clang_major__ >= 15 // Xcode 15+
buttons[@"pageup"] = @(XCUIRemoteButtonPageUp); // 9
buttons[@"pagedown"] = @(XCUIRemoteButtonPageDown); // 10
buttons[@"guide"] = @(XCUIRemoteButtonGuide); // 11
#endif
Comment on lines +44 to +48
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tvOS button availability is gated using __clang_major__ (Xcode/clang version), but the PR description says the guard should be based on whether the SDK headers include the enum values. Using clang version is an indirect proxy and can unnecessarily disable support when building with an SDK that already contains these enum cases. Prefer guarding with the SDK availability macros (e.g. __TV_OS_VERSION_MAX_ALLOWED) so the checks reflect the actual headers being compiled against.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the _VERSION_MAX_ALLOWED macros anywhere, and in this case the clang version actually has a stricter requirement than the tvOS version

#if __clang_major__ >= 17 // likely Xcode 16.3+
if (@available(tvOS 18.1, *)) {
buttons[@"fourcolors"] = @(XCUIRemoteButtonFourColors); // 12
buttons[@"onetwothree"] = @(XCUIRemoteButtonOneTwoThree); // 13
buttons[@"tvprovider"] = @(XCUIRemoteButtonTVProvider); // 14
}
#endif
Comment on lines +49 to +55
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue for the tvOS 18.1+ remote buttons: __clang_major__ is a brittle proxy for SDK support. Guard these enum references with the corresponding tvOS SDK max-allowed macro (and keep the @available(tvOS 18.1, *) runtime gate) to ensure compilation is tied to header availability rather than compiler version.

Copilot uses AI. Check for mistakes.
result = [buttons copy];
});
return result;
}
#else
NSDictionary<NSString *, NSNumber *> *fb_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 __clang_major__ >= 15 // likely Xcode 15+
if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonAction]) {
buttons[@"action"] = @(XCUIDeviceButtonAction);
buttons[@"action"] = @(XCUIDeviceButtonAction); // 4
}
#endif
#if defined(XCUIDeviceButtonCamera)
#if !TARGET_OS_SIMULATOR
#if (!TARGET_OS_SIMULATOR && __clang_major__ >= 16) // likely Xcode 16+
if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonCamera]) {
buttons[@"camera"] = @(XCUIDeviceButtonCamera);
}
Comment on lines 71 to 80
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS, gating XCUIDeviceButtonAction/XCUIDeviceButtonCamera behind __clang_major__ ties the feature to compiler version rather than SDK header availability. To match the intent (“guard behind whether the used Xcode SDK includes its enum”), use the relevant __IPHONE_OS_VERSION_MAX_ALLOWED checks (plus the existing @available(iOS 16.0, *) runtime guard) so the code compiles/behaves correctly across different toolchains.

Copilot uses AI. Check for mistakes.
#endif
#endif
}
#endif
result = [buttons copy];
});
return result;
}
#endif

@implementation XCUIDevice (FBHelpers)

Expand Down Expand Up @@ -247,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
Expand All @@ -257,71 +282,23 @@ - (BOOL)fb_pressButton:(NSString *)buttonName
#if !TARGET_OS_TV
return [self fb_pressButton:buttonName error:error];
#else
NSMutableArray<NSString *> *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"];

if (remoteButton == -1) {
NSDictionary<NSString *, NSNumber *> *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, supportedButtonNames]
withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys]
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
}
Expand All @@ -330,12 +307,13 @@ - (BOOL)fb_pressButton:(NSString *)buttonName
- (BOOL)fb_pressButton:(NSString *)buttonName
error:(NSError **)error
{
NSDictionary<NSString *, NSNumber *> *availableButtons = availableButtonNames();
NSDictionary<NSString *, NSNumber *> *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]];
Expand Down
Loading