FBElementUtils.m 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /**
  2. * Copyright (c) 2015-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import <objc/runtime.h>
  10. #import "FBXCAccessibilityElement.h"
  11. #import "FBElementUtils.h"
  12. #import "FBElementTypeTransformer.h"
  13. NSString *const FBUnknownAttributeException = @"FBUnknownAttributeException";
  14. static NSString *const WD_PREFIX = @"wd";
  15. static NSString *const OBJC_PROP_GETTER_PREFIX = @"G";
  16. static NSString *const OBJC_PROP_ATTRIBS_SEPARATOR = @",";
  17. @implementation FBElementUtils
  18. + (NSSet<NSString *> *)selectorNamesWithProtocol:(Protocol *)protocol
  19. {
  20. unsigned int count;
  21. struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, YES, YES, &count);
  22. NSMutableSet<NSString *> *result = [NSMutableSet set];
  23. for (unsigned int i = 0; i < count; i++) {
  24. SEL sel = methods[i].name;
  25. if (nil != sel) {
  26. [result addObject:NSStringFromSelector(sel)];
  27. }
  28. }
  29. free(methods);
  30. return result.copy;
  31. }
  32. + (NSString *)wdAttributeNameForAttributeName:(NSString *)name
  33. {
  34. NSAssert(name.length > 0, @"Attribute name cannot be empty", nil);
  35. NSDictionary *attributeNamesMapping = [self.class wdAttributeNamesMapping];
  36. NSString *result = attributeNamesMapping[name];
  37. if (nil == result) {
  38. NSString *description = [NSString stringWithFormat:@"The attribute '%@' is unknown. Valid attribute names are: %@", name, [attributeNamesMapping.allKeys sortedArrayUsingSelector:@selector(compare:)]];
  39. @throw [NSException exceptionWithName:FBUnknownAttributeException reason:description userInfo:@{}];
  40. return nil;
  41. }
  42. return result;
  43. }
  44. + (NSSet<NSNumber *> *)uniqueElementTypesWithElements:(NSArray<id<FBElement>> *)elements
  45. {
  46. NSMutableSet *matchingTypes = [NSMutableSet set];
  47. [elements enumerateObjectsUsingBlock:^(id<FBElement> element, NSUInteger elementIdx, BOOL *stopElementsEnum) {
  48. [matchingTypes addObject: @([FBElementTypeTransformer elementTypeWithTypeName:element.wdType])];
  49. }];
  50. return matchingTypes.copy;
  51. }
  52. + (NSDictionary<NSString *, NSString *> *)wdAttributeNamesMapping
  53. {
  54. static NSDictionary *attributeNamesMapping;
  55. static dispatch_once_t onceToken;
  56. dispatch_once(&onceToken, ^{
  57. NSMutableDictionary *wdPropertyGettersMapping = [NSMutableDictionary new];
  58. unsigned int propsCount = 0;
  59. Protocol * aProtocol = objc_getProtocol(protocol_getName(@protocol(FBElement)));
  60. objc_property_t *properties = protocol_copyPropertyList(aProtocol, &propsCount);
  61. for (unsigned int i = 0; i < propsCount; ++i) {
  62. objc_property_t property = properties[i];
  63. const char *name = property_getName(property);
  64. NSString *nsName = [NSString stringWithUTF8String:name];
  65. if (nil == nsName || ![nsName hasPrefix:WD_PREFIX]) {
  66. continue;
  67. }
  68. [wdPropertyGettersMapping setObject:[NSNull null] forKey:nsName];
  69. const char *c_attributes = property_getAttributes(property);
  70. NSString *attributes = [NSString stringWithUTF8String:c_attributes];
  71. if (nil == attributes) {
  72. continue;
  73. }
  74. // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html
  75. NSArray *splitAttrs = [attributes componentsSeparatedByString:OBJC_PROP_ATTRIBS_SEPARATOR];
  76. for (NSString *part in splitAttrs) {
  77. if ([part hasPrefix:OBJC_PROP_GETTER_PREFIX]) {
  78. [wdPropertyGettersMapping setObject:[part substringFromIndex:1] forKey:nsName];
  79. break;
  80. }
  81. }
  82. }
  83. free(properties);
  84. NSMutableDictionary *resultCache = [NSMutableDictionary new];
  85. for (NSString *propName in wdPropertyGettersMapping) {
  86. if ([[wdPropertyGettersMapping valueForKey:propName] isKindOfClass:NSNull.class]) {
  87. // no getter
  88. [resultCache setValue:propName forKey:propName];
  89. } else {
  90. // has getter method
  91. [resultCache setValue:[wdPropertyGettersMapping objectForKey:propName] forKey:propName];
  92. }
  93. NSString *aliasName;
  94. if (propName.length <= WD_PREFIX.length + 1) {
  95. aliasName = [NSString stringWithFormat:@"%@",
  96. [propName substringWithRange:NSMakeRange(WD_PREFIX.length, 1)].lowercaseString];
  97. } else {
  98. NSString *propNameWithoutPrefix = [propName substringFromIndex:WD_PREFIX.length];
  99. NSString *firstPropNameCharacter = [propNameWithoutPrefix substringWithRange:NSMakeRange(0, 1)];
  100. if (![propNameWithoutPrefix isEqualToString:[propNameWithoutPrefix uppercaseString]]) {
  101. // Lowercase the first character for the alias if the property name is not an uppercase abbreviation
  102. firstPropNameCharacter = firstPropNameCharacter.lowercaseString;
  103. }
  104. aliasName = [NSString stringWithFormat:@"%@%@", firstPropNameCharacter, [propNameWithoutPrefix substringFromIndex:1]];
  105. }
  106. if ([[wdPropertyGettersMapping valueForKey:propName] isKindOfClass:NSNull.class]) {
  107. // no getter
  108. [resultCache setValue:propName forKey:aliasName];
  109. } else {
  110. // has getter method
  111. [resultCache setValue:[wdPropertyGettersMapping objectForKey:propName] forKey:aliasName];
  112. }
  113. }
  114. attributeNamesMapping = resultCache.copy;
  115. });
  116. return attributeNamesMapping;
  117. }
  118. + (NSString *)uidWithAccessibilityElement:(id<FBXCAccessibilityElement>)element
  119. {
  120. unsigned long long elementId = [self.class idWithAccessibilityElement:element];
  121. int processId = element.processIdentifier;
  122. if (elementId < 1 || processId < 1) {
  123. return nil;
  124. }
  125. uint8_t b[16] = {0};
  126. memcpy(b, &elementId, sizeof(long long));
  127. memcpy(b + sizeof(long long), &processId, sizeof(int));
  128. NSUUID *uuidValue = [[NSUUID alloc] initWithUUIDBytes:b];
  129. return uuidValue.UUIDString;
  130. }
  131. static BOOL FBShouldUsePayloadForUIDExtraction = YES;
  132. static dispatch_once_t oncePayloadToken;
  133. + (unsigned long long)idWithAccessibilityElement:(id<FBXCAccessibilityElement>)element
  134. {
  135. dispatch_once(&oncePayloadToken, ^{
  136. FBShouldUsePayloadForUIDExtraction = [(NSObject *)element respondsToSelector:@selector(payload)];
  137. });
  138. return FBShouldUsePayloadForUIDExtraction
  139. ? [[element.payload objectForKey:@"uid.elementID"] longLongValue]
  140. : [[(NSObject *)element valueForKey:@"_elementID"] longLongValue];
  141. }
  142. @end