Skip to content

Commit

Permalink
feat: Exclude the tree hierarchy from snapshots that don't need it
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Jan 14, 2025
1 parent 194ddf1 commit b263b02
Show file tree
Hide file tree
Showing 38 changed files with 208 additions and 445 deletions.
4 changes: 2 additions & 2 deletions WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *
// and conatins at least one text view
__block NSUInteger buttonsCount = 0;
__block NSUInteger textViewsCount = 0;
id<FBXCElementSnapshot> snapshot = candidate.fb_cachedSnapshot ?: candidate.fb_takeSnapshot;
id<FBXCElementSnapshot> snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_takeSnapshot:YES];
[snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
XCUIElementType curType = descendant.elementType;
if (curType == XCUIElementTypeButton) {
Expand All @@ -73,7 +73,7 @@ - (XCUIElement *)fb_alertElement
if (nil == alert) {
return nil;
}
id<FBXCElementSnapshot> alertSnapshot = alert.fb_cachedSnapshot ?: alert.fb_takeSnapshot;
id<FBXCElementSnapshot> alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_takeSnapshot:YES];

if (alertSnapshot.elementType == XCUIElementTypeAlert) {
return alert;
Expand Down
15 changes: 7 additions & 8 deletions WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ - (NSDictionary *)fb_tree:(nullable NSSet<NSString *> *)excludedAttributes
// This set includes XCTest-specific internal attribute names,
// while the `excludedAttributes` arg contains human-readable ones
NSMutableSet* includedAttributeNames = [NSMutableSet setWithArray:FBCustomAttributeNames()];
[includedAttributeNames addObjectsFromArray:FBStandardAttributeNames()];
if (nil != excludedAttributes) {
for (NSString *attr in excludedAttributes) {
NSString *mappedName = [customExclusionAttributesMap() objectForKey:attr];
Expand All @@ -189,18 +188,18 @@ - (NSDictionary *)fb_tree:(nullable NSSet<NSString *> *)excludedAttributes
}
}
id<FBXCElementSnapshot> snapshot = nil == excludedAttributes
? [self fb_snapshotWithAllAttributesAndMaxDepth:nil]
: [self fb_snapshotWithAttributes:[includedAttributeNames allObjects] maxDepth:nil];
? [self fb_snapshotWithAllAttributes:YES]
: [self fb_snapshotWithCustomAttributes:[includedAttributeNames allObjects]
exludingStandardAttributes:NO
inDepth:YES];
return [self.class dictionaryForElement:snapshot
recursive:YES
excludedAttributes:excludedAttributes];
}

- (NSDictionary *)fb_accessibilityTree
{
id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
? self.lastSnapshot
: [self fb_snapshotWithAllAttributesAndMaxDepth:nil];
id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithAllAttributes:YES];
return [self.class accessibilityInfoForElement:snapshot];
}

Expand Down Expand Up @@ -445,8 +444,8 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames

id extractedElement = extractIssueProperty(issue, @"element");

id<FBXCElementSnapshot> elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_takeSnapshot];
NSDictionary *elementAttributes = elementSnapshot
id<FBXCElementSnapshot> elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_takeSnapshot:NO];
NSDictionary *elementAttributes = elementSnapshot
? [self.class dictionaryForElement:elementSnapshot
recursive:NO
excludedAttributes:customAttributesToExclude]
Expand Down
5 changes: 3 additions & 2 deletions WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ @implementation XCUIElement (FBAccessibility)

- (BOOL)fb_isAccessibilityElement
{
id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName]
maxDepth:@1];
id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithCustomAttributes:@[FB_XCAXAIsElementAttributeName]
exludingStandardAttributes:YES
inDepth:NO];
return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isAccessibilityElement;
}

Expand Down
3 changes: 0 additions & 3 deletions WebDriverAgentLib/Categories/XCUIElement+FBCaching.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ NS_ASSUME_NONNULL_BEGIN

@interface XCUIElement (FBCaching)

/*! This property is set to YES if the given element has been resolved from the cache, so it is safe to use the `lastSnapshot` property */
@property (nullable, nonatomic) NSNumber *fb_isResolvedFromCache;

@property (nonatomic, readonly) NSString *fb_cacheId;

@end
Expand Down
23 changes: 1 addition & 22 deletions WebDriverAgentLib/Categories/XCUIElement+FBCaching.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@

@implementation XCUIElement (FBCaching)

static char XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY;

@dynamic fb_isResolvedFromCache;

- (void)setFb_isResolvedFromCache:(NSNumber *)isResolvedFromCache
{
objc_setAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY, isResolvedFromCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSNumber *)fb_isResolvedFromCache
{
return (NSNumber *)objc_getAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY);
}

static char XCUIELEMENT_CACHE_ID_KEY;

@dynamic fb_cacheId;
Expand All @@ -43,14 +29,7 @@ - (NSString *)fb_cacheId
return (NSString *)result;
}

NSString *uid;
if ([self isKindOfClass:XCUIApplication.class]) {
uid = self.fb_uid;
} else {
id<FBXCElementSnapshot> snapshot = self.fb_cachedSnapshot ?: self.fb_takeSnapshot;
uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot];
}

NSString *uid = self.fb_uid;
objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return uid;
}
Expand Down
2 changes: 1 addition & 1 deletion WebDriverAgentLib/Categories/XCUIElement+FBFind.m
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ @implementation XCUIElement (FBFind)
matchingSnapshots = @[snapshot];
}
return [self fb_filterDescendantsWithSnapshots:matchingSnapshots
selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot]
selfUID:self.fb_uid
onlyChildren:NO];
}

Expand Down
180 changes: 4 additions & 176 deletions WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,196 +25,24 @@ @implementation XCUIElement (FBIsVisible)

- (BOOL)fb_isVisible
{
id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName]
maxDepth:@1];
id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithCustomAttributes:@[FB_XCAXAIsVisibleAttributeName]
exludingStandardAttributes:YES
inDepth:NO];
return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible;
}

@end

@implementation FBXCElementSnapshotWrapper (FBIsVisible)

+ (NSString *)fb_uniqIdWithSnapshot:(id<FBXCElementSnapshot>)snapshot
{
return [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot] ?: [NSString stringWithFormat:@"%p", (void *)snapshot];
}

- (nullable NSNumber *)fb_cachedVisibilityValue
{
NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache;
if (nil == cache) {
return nil;
}

NSDictionary<NSString *, NSNumber *> *result = cache[@(self.generation)];
if (nil == result) {
// There is no need to keep the cached data for the previous generations
[cache removeAllObjects];
cache[@(self.generation)] = [NSMutableDictionary dictionary];
return nil;
}
return result[[self.class fb_uniqIdWithSnapshot:self.snapshot]];
}

- (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible
forAncestors:(nullable NSArray<id<FBXCElementSnapshot>> *)ancestors
{
NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache;
if (nil == cache) {
return isVisible;
}
NSMutableDictionary<NSString *, NSNumber *> *destination = cache[@(self.generation)];
if (nil == destination) {
return isVisible;
}

NSNumber *visibleObj = [NSNumber numberWithBool:isVisible];
destination[[self.class fb_uniqIdWithSnapshot:self.snapshot]] = visibleObj;
if (isVisible && nil != ancestors) {
// if an element is visible then all its ancestors must be visible as well
for (id<FBXCElementSnapshot> ancestor in ancestors) {
NSString *ancestorId = [self.class fb_uniqIdWithSnapshot:ancestor];
if (nil == destination[ancestorId]) {
destination[ancestorId] = visibleObj;
}
}
}
return isVisible;
}

- (CGRect)fb_frameInContainer:(id<FBXCElementSnapshot>)container
hierarchyIntersection:(nullable NSValue *)intersectionRectange
{
CGRect currentRectangle = nil == intersectionRectange ? self.frame : [intersectionRectange CGRectValue];
id<FBXCElementSnapshot> parent = self.parent;
CGRect parentFrame = parent.frame;
CGRect containerFrame = container.frame;
if (CGSizeEqualToSize(parentFrame.size, CGSizeZero) &&
CGPointEqualToPoint(parentFrame.origin, CGPointZero)) {
// Special case (or XCTest bug). Shift the origin and return immediately after shift
id<FBXCElementSnapshot> nextParent = parent.parent;
BOOL isGrandparent = YES;
while (nextParent && nextParent != container) {
CGRect nextParentFrame = nextParent.frame;
if (isGrandparent &&
CGSizeEqualToSize(nextParentFrame.size, CGSizeZero) &&
CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) {
// Double zero-size container inclusion means that element coordinates are absolute
return CGRectIntersection(currentRectangle, containerFrame);
}
isGrandparent = NO;
if (!CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) {
currentRectangle.origin.x += nextParentFrame.origin.x;
currentRectangle.origin.y += nextParentFrame.origin.y;
return CGRectIntersection(currentRectangle, containerFrame);
}
nextParent = nextParent.parent;
}
return CGRectIntersection(currentRectangle, containerFrame);
}
// Skip parent containers if they are outside of the viewport
CGRect intersectionWithParent = CGRectIntersectsRect(parentFrame, containerFrame) || parent.elementType != XCUIElementTypeOther
? CGRectIntersection(currentRectangle, parentFrame)
: currentRectangle;
if (CGRectIsEmpty(intersectionWithParent) &&
parent != container &&
self.elementType == XCUIElementTypeOther) {
// Special case (or XCTest bug). Shift the origin
if (CGSizeEqualToSize(parentFrame.size, containerFrame.size) ||
// The size might be inverted in landscape
CGSizeEqualToSize(parentFrame.size, CGSizeMake(containerFrame.size.height, containerFrame.size.width)) ||
CGSizeEqualToSize(self.frame.size, CGSizeZero)) {
// Covers ActivityListView and RemoteBridgeView cases
currentRectangle.origin.x += parentFrame.origin.x;
currentRectangle.origin.y += parentFrame.origin.y;
return CGRectIntersection(currentRectangle, containerFrame);
}
}
if (CGRectIsEmpty(intersectionWithParent) || parent == container) {
return intersectionWithParent;
}
return [[FBXCElementSnapshotWrapper ensureWrapped:parent] fb_frameInContainer:container
hierarchyIntersection:[NSValue valueWithCGRect:intersectionWithParent]];
}

- (BOOL)fb_hasAnyVisibleLeafs
{
NSArray<id<FBXCElementSnapshot>> *children = self.children;
if (0 == children.count) {
return self.fb_isVisible;
}

for (id<FBXCElementSnapshot> child in children) {
if ([FBXCElementSnapshotWrapper ensureWrapped:child].fb_hasAnyVisibleLeafs) {
return YES;
}
}

return NO;
}

- (BOOL)fb_isVisible
{
NSNumber *isVisible = self.additionalAttributes[FB_XCAXAIsVisibleAttribute];
if (isVisible != nil) {
return isVisible.boolValue;
}

NSNumber *cachedValue = [self fb_cachedVisibilityValue];
if (nil != cachedValue) {
return [cachedValue boolValue];
}

CGRect selfFrame = self.frame;
if (CGRectIsEmpty(selfFrame)) {
return [self fb_cacheVisibilityWithValue:NO forAncestors:nil];
}

NSArray<id<FBXCElementSnapshot>> *ancestors = self.fb_ancestors;
if ([FBConfiguration shouldUseTestManagerForVisibilityDetection]) {
BOOL visibleAttrValue = [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsVisibleAttributeName] boolValue];
return [self fb_cacheVisibilityWithValue:visibleAttrValue forAncestors:ancestors];
}

id<FBXCElementSnapshot> parentWindow = ancestors.count > 1 ? [ancestors objectAtIndex:ancestors.count - 2] : nil;
CGRect visibleRect = selfFrame;
if (nil != parentWindow) {
visibleRect = [self fb_frameInContainer:parentWindow hierarchyIntersection:nil];
}
if (CGRectIsEmpty(visibleRect)) {
return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors];
}
CGPoint midPoint = CGPointMake(visibleRect.origin.x + visibleRect.size.width / 2,
visibleRect.origin.y + visibleRect.size.height / 2);
id<FBXCAccessibilityElement> hitElement = [FBActiveAppDetectionPoint axElementWithPoint:midPoint];
if (nil != hitElement) {
if (FBIsAXElementEqualToOther(self.accessibilityElement, hitElement)) {
return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
}
for (id<FBXCElementSnapshot> ancestor in ancestors) {
if (FBIsAXElementEqualToOther(hitElement, ancestor.accessibilityElement)) {
return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
}
}
}
if (self.children.count > 0) {
if (nil != hitElement) {
for (id<FBXCElementSnapshot> descendant in self._allDescendants) {
if (FBIsAXElementEqualToOther(hitElement, descendant.accessibilityElement)) {
return [self fb_cacheVisibilityWithValue:YES
forAncestors:[FBXCElementSnapshotWrapper ensureWrapped:descendant].fb_ancestors];
}
}
}
if (self.fb_hasAnyVisibleLeafs) {
return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
}
} else if (nil == hitElement) {
// Sometimes XCTest returns nil for leaf elements hit test even if such elements are hittable
// Assume such elements are visible if their rectInContainer is visible
return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
}
return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors];
return [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsVisibleAttributeName] boolValue];
}

@end
4 changes: 1 addition & 3 deletions WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ @implementation XCUIElement (FBPickerWheel)

- (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)error
{
id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
? self.lastSnapshot
: self.fb_takeSnapshot;
id<FBXCElementSnapshot> snapshot = [self fb_takeSnapshot:NO];
NSString *previousValue = snapshot.value;
XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)];
Expand Down
Loading

0 comments on commit b263b02

Please sign in to comment.