Skip to content
Merged
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
4 changes: 4 additions & 0 deletions WebDriverAgent.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,7 @@
FBCAFE000000000000003105 /* FBMobilerunActionsCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000003104 /* FBMobilerunActionsCommands.m */; };
FBCAFE000000000000003106 /* FBMobilerunActionsCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000003104 /* FBMobilerunActionsCommands.m */; };
FBCAFE000000000000003202 /* FBMobilerunActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000003201 /* FBMobilerunActionsIntegrationTests.m */; };
FBCAFE000000000000003402 /* FBMobilerunA11yIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000003401 /* FBMobilerunA11yIntegrationTests.m */; };
FBCAFE000000000000004002 /* FBVideoStreamSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000004001 /* FBVideoStreamSessionTests.m */; };
FBCAFE000000000000005103 /* FBBroadcastSampleHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000005102 /* FBBroadcastSampleHandler.m */; };
FBCAFE000000000000005106 /* FBExtBroadcastClient.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000005105 /* FBExtBroadcastClient.m */; };
Expand Down Expand Up @@ -2082,6 +2083,7 @@
FBCAFE000000000000003101 /* FBMobilerunActionsCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBMobilerunActionsCommands.h; sourceTree = "<group>"; };
FBCAFE000000000000003104 /* FBMobilerunActionsCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBMobilerunActionsCommands.m; sourceTree = "<group>"; };
FBCAFE000000000000003201 /* FBMobilerunActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBMobilerunActionsIntegrationTests.m; sourceTree = "<group>"; };
FBCAFE000000000000003401 /* FBMobilerunA11yIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBMobilerunA11yIntegrationTests.m; sourceTree = "<group>"; };
FBCAFE000000000000004001 /* FBVideoStreamSessionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBVideoStreamSessionTests.m; sourceTree = "<group>"; };
FBCAFE000000000000005002 /* WebDriverAgentBroadcast.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WebDriverAgentBroadcast.appex; sourceTree = BUILT_PRODUCTS_DIR; };
FBCAFE00000000000000500C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2908,6 +2910,7 @@
A1B2C3D41F001A00A1B0003 /* FBVoiceOverTests.m */,
71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */,
FBCAFE000000000000003201 /* FBMobilerunActionsIntegrationTests.m */,
FBCAFE000000000000003401 /* FBMobilerunA11yIntegrationTests.m */,
FBCAFE000000000000007330 /* FBMobilerunSocks5IntegrationTests.m */,
71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */,
7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */,
Expand Down Expand Up @@ -5206,6 +5209,7 @@
A1B2C3D41F001A00A1B0008 /* FBVoiceOverTests.m in Sources */,
71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */,
FBCAFE000000000000003202 /* FBMobilerunActionsIntegrationTests.m in Sources */,
FBCAFE000000000000003402 /* FBMobilerunA11yIntegrationTests.m in Sources */,
FBCAFE000000000000007331 /* FBMobilerunSocks5IntegrationTests.m in Sources */,
63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */,
719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */,
Expand Down
32 changes: 26 additions & 6 deletions WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,39 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSDictionary *)fb_accessibilityTree;

/**
Takes one snapshot of the application that gives up after 'timeout' seconds.
Unlike -snapshotWithError: it skips XCTest's element resolution and its
retries, each of which waits up to XCTest's 60s accessibility timeout.

@param timeout The accessibility timeout for this request, in seconds.
@param parameters Snapshot request parameters overriding the defaults, e.g.
a smaller FBSnapshotMaxDepthKey.
@param error Set when the snapshot failed or timed out.
@return The snapshot, or nil on failure.
*/
- (nullable id<FBXCElementSnapshot>)fb_snapshotWithTimeout:(NSTimeInterval)timeout
parameters:(NSDictionary<NSString *, id> *)parameters
error:(NSError **)error;

/**
Returns a compact accessibility state tailored for mobilerun: a recursive
accessibility tree (Android-style `A11YNode` shape) plus a `phone_state`
summary. It is built from a single standard snapshot and only reads
snapshot-resident attributes, so it deliberately avoids the expensive
visibility/accessibility/hittable resolution performed by `fb_tree`.
summary. It only reads snapshot-resident attributes, so it deliberately
avoids the expensive visibility/accessibility/hittable resolution performed
by `fb_tree`, and makes no accessibility requests of its own.

@param snapshot The application's snapshot, or nil when none could be taken:
the tree is then the application root alone, spanning the screen.
@param scale The factor the logical-point element bounds are multiplied by.
Pass the native screen scale for device pixels or 1 for plain points.
@return Dictionary with `a11y_tree` (nested `A11YNode` dictionaries) and
`phone_state` entries.
@param truncated Whether the snapshot is incomplete, reported as `truncated`.
@return Dictionary with `a11y_tree` (nested `A11YNode` dictionaries),
`phone_state` and `truncated` entries.
*/
- (NSDictionary *)fb_mobilerunA11yStateWithScale:(CGFloat)scale;
- (NSDictionary *)fb_mobilerunA11yStateWithSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
scale:(CGFloat)scale
truncated:(BOOL)truncated;

/**
Return application elements tree in a form of xml string
Expand Down
80 changes: 73 additions & 7 deletions WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#import "FBXCElementSnapshotWrapper+Helpers.h"
#import "FBXCAXClientProxy.h"
#import "FBXMLGenerationOptions.h"
#import "XCAXClient_iOS+FBSnapshotReqParams.h"
#import "XCTMessagingChannel_RunnerToDaemon-Protocol.h"
#import "XCTestPrivateSymbols.h"
#import "XCTRunnerDaemonSession.h"
Expand Down Expand Up @@ -350,6 +351,9 @@ + (NSDictionary *)accessibilityInfoForElement:(id<FBXCElementSnapshot>)snapshot

#pragma mark - mobilerun accessibility state

// How long the root-only fallback waits for SpringBoard's frame, in seconds.
static const NSTimeInterval FBMobilerunSystemFrameTimeout = 2.;

// Converts a logical-point frame into integer device-pixel bounds matching the
// `A11YBounds` shape (left/top/right/bottom) expected by mobilerun.
static NSDictionary *FBMobilerunBoundsFromFrame(CGRect frame, CGFloat scale)
Expand Down Expand Up @@ -411,18 +415,43 @@ static BOOL FBMobilerunIsCheckableType(XCUIElementType type)
}
}

- (NSDictionary *)fb_mobilerunA11yStateWithScale:(CGFloat)scale
- (id<FBXCElementSnapshot>)fb_snapshotWithTimeout:(NSTimeInterval)timeout
parameters:(NSDictionary<NSString *, id> *)parameters
error:(NSError **)error
{
FBXCAXClientProxy *client = FBXCAXClientProxy.sharedClient;
// The timeout applies to every accessibility request of the runner, so it is put back right
// after this one. AXTimeout keeps reporting XCTest's initial value, which is what to restore.
NSTimeInterval defaultTimeout = client.AXTimeout;
NSError *timeoutError;
if (![client setAXTimeout:timeout error:&timeoutError]) {
[FBLogger logFmt:@"Could not limit the snapshot to %.1fs: %@", timeout, timeoutError];
}
id<FBXCElementSnapshot> snapshot = [client snapshotForElement:self.accessibilityElement
attributes:FBStandardAttributeNames()
parameters:parameters
error:error];
if (![client setAXTimeout:defaultTimeout error:&timeoutError]) {
[FBLogger logFmt:@"Could not restore the %.1fs accessibility timeout: %@", defaultTimeout, timeoutError];
}
return snapshot;
}

- (NSDictionary *)fb_mobilerunA11yStateWithSnapshot:(id<FBXCElementSnapshot>)snapshot
scale:(CGFloat)scale
truncated:(BOOL)truncated
{
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
NSString *bundleId = self.bundleID ?: @"";

// Side-channel populated during the single tree walk so `phone_state` needs no
// extra snapshots or attribute queries.
NSMutableDictionary *context = [NSMutableDictionary dictionary];
NSDictionary *tree = [self.class fb_a11yNodeForSnapshot:snapshot
scale:scale
packageName:bundleId
context:context];
NSDictionary *tree = nil == snapshot
? [self.class fb_a11yRootNodeWithScale:scale packageName:bundleId]
: [self.class fb_a11yNodeForSnapshot:snapshot
scale:scale
packageName:bundleId
context:context];

NSMutableDictionary *phoneState = [NSMutableDictionary dictionary];
NSDictionary *focusedElement = context[@"focusedElement"];
Expand All @@ -442,8 +471,45 @@ - (NSDictionary *)fb_mobilerunA11yStateWithScale:(CGFloat)scale
phoneState[@"isEditable"] = @([context[@"isEditable"] boolValue]);

return @{
@"a11y_tree": tree ?: [NSNull null],
@"a11y_tree": tree,
@"phone_state": phoneState.copy,
@"truncated": @(truncated),
};
}

// Stands in for the tree when no snapshot could be taken: clients reject a null tree, and this
// still names the application. Its frame is unknown without a snapshot, and so is the interface
// orientation, so the root spans a square covering the screen either way round - clients drop a
// childless root that lies mostly off-screen.
+ (NSDictionary *)fb_a11yRootNodeWithScale:(CGFloat)scale packageName:(NSString *)bundleId
{
// SpringBoard spans the screen and answers even while the app is stuck. The runner's own
// UIScreen is no substitute on a device: without a launch screen the runner runs in the
// letterboxed compatibility mode, where bounds and nativeBounds both describe a 320x480 screen.
id<FBXCElementSnapshot> systemSnapshot = [XCUIApplication.fb_systemApplication
fb_snapshotWithTimeout:FBMobilerunSystemFrameTimeout
parameters:@{FBSnapshotMaxDepthKey: @1}
error:nil];
CGSize screenSize = nil == systemSnapshot ? UIScreen.mainScreen.bounds.size : systemSnapshot.frame.size;
CGFloat side = MAX(screenSize.width, screenSize.height);
return @{
@"boundsInScreen": FBMobilerunBoundsFromFrame(CGRectMake(0, 0, side, side), scale),
@"className": [FBElementTypeTransformer stringWithElementType:XCUIElementTypeApplication],
@"resourceId": @"",
@"text": @"",
@"contentDescription": @"",
@"packageName": bundleId,
@"isCheckable": @NO,
@"isChecked": @NO,
@"isClickable": @NO,
@"isEnabled": @YES,
@"isFocusable": @NO,
@"isFocused": @NO,
@"isScrollable": @NO,
@"isLongClickable": @NO,
@"isPassword": @NO,
@"isSelected": @NO,
@"children": @[],
};
}

Expand Down
58 changes: 47 additions & 11 deletions WebDriverAgentLib/Commands/FBMobilerunA11yCommands.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,60 @@

NS_ASSUME_NONNULL_BEGIN

@class FBCommandStatus;
@class FBSession;

/**
How long one full accessibility snapshot of the active app may take, in seconds. XCTest's own
limit is 60s per request and it retries; an app that is stuck or has a huge hierarchy (TikTok on
older iPhones) otherwise holds the request - and every request queued behind it - for minutes.
*/
extern const NSTimeInterval FBMobilerunStateSnapshotTimeout;

/** The limit for the shallow snapshot taken when the full one did not finish. */
extern const NSTimeInterval FBMobilerunStateShallowSnapshotTimeout;

/** How far the shallow fallback snapshot descends: tree levels below the app, children per element. */
extern const int FBMobilerunStateShallowSnapshotMaxDepth;
extern const int FBMobilerunStateShallowSnapshotMaxChildren;

typedef NS_ENUM(NSUInteger, FBMobilerunSnapshotDepth) {
FBMobilerunSnapshotDepthFull,
FBMobilerunSnapshotDepthShallow,
};

/** What the state assembly needs to know about an application. XCUIApplication conforms. */
@protocol FBMobilerunStateApplication <NSObject>
@property (nonatomic, readonly) int processID;
@end

@interface FBMobilerunA11yCommands : NSObject <FBCommandHandler>

/**
Builds the GET /mobilerun/state value by running 'attempt' - one snapshot walk of the active
application - and retrying it once when the snapshot fails (FBStaleElementException).
Builds the GET /mobilerun/state value for the active application within a bounded time:
a full snapshot, else a shallow one, else the application root alone. 'truncated' says whether
the tree is incomplete; 'phone_state.packageName' names the active application either way.

@param scale the factor logical-point bounds are multiplied by
@param session the session whose active application to use, or nil to detect it
*/
+ (NSDictionary *)stateOfActiveApplicationWithScale:(CGFloat)scale session:(nullable FBSession *)session;

/**
The decision logic behind 'stateOfActiveApplicationWithScale:session:', exposed for unit testing.

Exposed for unit testing; the route handler passes a block that resolves the active application
anew on every call.
Snapshots the active application in full. When that fails and another application has taken over
meanwhile (right after a Home press, say), snapshots that one in full instead; never the same
application twice, since a timed-out snapshot would only time out again. Then falls back to a
shallow snapshot, and finally to no snapshot at all.

@param attempt returns the state dictionary; may throw
@param failure set when both attempts failed to snapshot the application
@return the state dictionary, or nil with 'failure' set. Exceptions other than a failed snapshot
are rethrown unchanged.
@param activeApplication resolves the active application; called at most twice
@param snapshot takes a snapshot of the given depth, or returns nil when it failed
@param build assembles the state from the application, its snapshot (nil when none succeeded) and
whether the snapshot is incomplete
*/
+ (nullable NSDictionary *)stateWithSnapshotAttempt:(NSDictionary *(NS_NOESCAPE ^)(void))attempt
failure:(FBCommandStatus *_Nullable *_Nullable)failure;
+ (NSDictionary *)stateWithActiveApplication:(id<FBMobilerunStateApplication> (NS_NOESCAPE ^)(void))activeApplication
snapshot:(id _Nullable (NS_NOESCAPE ^)(id<FBMobilerunStateApplication> application, FBMobilerunSnapshotDepth depth))snapshot
build:(NSDictionary *(NS_NOESCAPE ^)(id<FBMobilerunStateApplication> application, id _Nullable snapshot, BOOL truncated))build;

@end

Expand Down
Loading
Loading