Skip to content
Open
Show file tree
Hide file tree
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
81 changes: 52 additions & 29 deletions WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#import "XCUIElement+FBWebDriverAttributes.h"
#import "XCUIElementQuery.h"
#import "FBElementHelpers.h"
#import "XCUIElement+FBUID.h"

static NSString* const FBUnknownBundleId = @"unknown";

Expand Down Expand Up @@ -180,7 +181,7 @@ - (NSDictionary *)fb_tree
- (NSDictionary *)fb_tree:(nullable NSSet<NSString *> *)excludedAttributes
{
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
return [self.class dictionaryForElement:snapshot
return [self dictionaryForElement:snapshot
recursive:YES
excludedAttributes:excludedAttributes];
}
Expand All @@ -191,9 +192,9 @@ - (NSDictionary *)fb_accessibilityTree
return [self.class accessibilityInfoForElement:snapshot];
}

+ (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
recursive:(BOOL)recursive
excludedAttributes:(nullable NSSet<NSString *> *)excludedAttributes
- (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
recursive:(BOOL)recursive
excludedAttributes:(nullable NSSet<NSString *> *)excludedAttributes
{
NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType];
Expand All @@ -203,10 +204,25 @@ + (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue);
info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel);
info[@"rect"] = wrappedSnapshot.wdRect;

NSDictionary<NSString *, NSString *(^)(void)> *attributeBlocks = [self fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot
excludedAttributes:excludedAttributes];

// Flatten the tree to get all snapshots
NSArray<id<FBXCElementSnapshot>> *allSnapshots = [self.class fb_flattenTree:snapshot];
NSArray<XCUIElement *> *resolvedElements = [self fb_filterDescendantsWithSnapshots:allSnapshots onlyChildren:NO];

NSMutableDictionary<NSString *, XCUIElement *> *uidToElement = [NSMutableDictionary dictionary];
for (XCUIElement *element in resolvedElements) {
NSString *uid = element.fb_uid;
if (uid != nil) {
uidToElement[uid] = element;
}
}

NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:wrappedSnapshot.snapshot];
XCUIElement *resolvedElement = uid != nil ? uidToElement[uid] : nil;

NSDictionary<NSString *, NSString *(^)(void)> *attributeBlocks = [self.class fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot
resolvedElement:resolvedElement
excludedAttributes:excludedAttributes];

NSSet *nonPrefixedKeys = [NSSet setWithObjects:
FBExclusionAttributeFrame,
Expand All @@ -215,14 +231,14 @@ + (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
nil];

for (NSString *key in attributeBlocks) {
if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) {
NSString *value = ((NSString * (^)(void))attributeBlocks[key])();
if ([nonPrefixedKeys containsObject:key]) {
info[key] = value;
} else {
info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value;
}
if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) {
NSString *value = ((NSString * (^)(void))attributeBlocks[key])();
if ([nonPrefixedKeys containsObject:key]) {
info[key] = value;
} else {
info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value;
}
}
}

if (!recursive) {
Expand All @@ -235,20 +251,26 @@ + (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
for (id<FBXCElementSnapshot> childSnapshot in childElements) {
@autoreleasepool {
[info[@"children"] addObject:[self dictionaryForElement:childSnapshot
recursive:YES
excludedAttributes:excludedAttributes]];
recursive:YES
excludedAttributes:excludedAttributes]];
}
}
}
return info;
}

// Helper used by `dictionaryForElement:` to assemble attribute value blocks,
// including both common attributes and conditionally included ones like placeholderValue.
+ (NSDictionary<NSString *, NSString *(^)(void)> *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot
excludedAttributes:(nullable NSSet<NSString *> *)excludedAttributes

+ (NSArray<id<FBXCElementSnapshot>> *)fb_flattenTree:(id<FBXCElementSnapshot>)snapshot
{
NSMutableArray *result = [NSMutableArray arrayWithObject:snapshot];
for (id<FBXCElementSnapshot> child in snapshot.children) {
[result addObjectsFromArray:[self fb_flattenTree:child]];
}
return result;
}

+ (NSDictionary<NSString *, NSString *(^)(void)> *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot
resolvedElement:(nullable XCUIElement *)resolvedElement
excludedAttributes:(nullable NSSet<NSString *> *)excludedAttributes
{
// Base attributes common to every element
NSMutableDictionary<NSString *, NSString *(^)(void)> *blocks =
Expand Down Expand Up @@ -281,14 +303,15 @@ + (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
return (NSString *)FBValueOrNull(wrappedSnapshot.wdPlaceholderValue);
};
}

// Adding isHittable, only if not excluded
if (excludedAttributes == nil || ![excludedAttributes containsObject:FBExclusionAttributeHittable]) {
blocks[FBExclusionAttributeHittable] = ^{
return [@([wrappedSnapshot isWDNativeHittable]) stringValue];
};
}


// Add isHittable only if resolvedElement is available and attribute not excluded
// TODO: gate behind explicit config flag
if (resolvedElement && (excludedAttributes == nil || ![excludedAttributes containsObject:FBExclusionAttributeHittable])) {
blocks[FBExclusionAttributeHittable] = ^{
return [@(resolvedElement.hittable) stringValue];
};
}

return [blocks copy];
}

Expand Down
34 changes: 3 additions & 31 deletions WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ @implementation XCUIElement (WebDriverAttributesForwarding)
- (id)fb_valueForWDAttributeName:(NSString *)name
{
NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:name];
if ([wdAttributeName isEqualToString:@"isHittable"]) {
return @(self.hittable);
}
id<FBXCElementSnapshot> snapshot = [self fb_snapshotForAttributeName:wdAttributeName];
return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_valueForWDAttributeName:name];
}
Expand Down Expand Up @@ -239,37 +242,6 @@ - (BOOL)isWDHittable
return nil == result ? NO : result.hittable;
}

/*! Whether the element is truly hittable based on XCUIElement.hittable */
- (BOOL)isWDNativeHittable
{
XCUIApplication *app = [XCUIApplication fb_activeApplication];
XCUIElementQuery *query = [app descendantsMatchingType:self.elementType];
XCUIElement *matchedElement = nil;

NSString *identifier = self.identifier;
NSString *label = self.label;
XCUIElementType type = self.elementType;

// Attempt to match by accessibilityIdentifier first
if (identifier.length > 0) {
XCUIElement *candidate = [query matchingIdentifier:identifier].element;
if (candidate.exists && candidate.elementType == type) {
matchedElement = candidate;
}
}

// If no match by identifier, try matching by label and type
if (!matchedElement.exists && label.length > 0) {
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(XCUIElement *el, NSDictionary *_) {
return [el.label isEqualToString:label] && el.elementType == type;
}];
matchedElement = [[query matchingPredicate:predicate] element];
}

// Return hittable status if element is found, otherwise NO
return matchedElement.exists ? matchedElement.hittable : NO;
}

- (NSDictionary *)wdRect
{
CGRect frame = self.wdFrame;
Expand Down
3 changes: 0 additions & 3 deletions WebDriverAgentLib/Routing/FBElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ NS_ASSUME_NONNULL_BEGIN
/*! Whether the element is considered hittable based on snapshot hit point */
@property (nonatomic, readonly, getter = isWDHittable) BOOL wdHittable;

/*! Whether the element is truly hittable based on XCUIElement.hittable */
@property (nonatomic, readonly, getter = isWDNativeHittable) BOOL wdNativeHittable;

/*! Element's index relatively to its parent. Starts from zero */
@property (nonatomic, readonly) NSUInteger wdIndex;

Expand Down
11 changes: 0 additions & 11 deletions WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,4 @@ - (void)testTextViewAttributes
XCTAssertEqualObjects(element.wdValue, @"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901");
}

- (void)testNativeHittableAttribute
{
XCUIElement *button = self.testedApplication.buttons[@"Button"];
XCTAssertTrue(button.exists);

id<FBXCElementSnapshot> snapshot = [button fb_standardSnapshot];
FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];

XCTAssertEqual([wrapped isWDNativeHittable], button.hittable);
}

@end