appium-webdriveragent 16.7.2 → 16.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +0 -10
- package/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +90 -0
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +4 -1
- package/WebDriverAgentLib/Commands/FBScreenshotCommands.m +2 -2
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +6 -5
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.h +25 -1
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +278 -92
- package/WebDriverAgentLib/Routing/FBRoute.h +9 -0
- package/WebDriverAgentLib/Routing/FBRoute.m +9 -0
- package/WebDriverAgentLib/Routing/FBSession.h +13 -0
- package/WebDriverAgentLib/Routing/FBSession.m +153 -22
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +2 -1
- package/WebDriverAgentLib/Routing/FBWebServer.m +26 -1
- package/WebDriverAgentLib/Routing/RouteResponse.m +1 -1
- package/WebDriverAgentLib/Utilities/FBConfiguration.h +11 -0
- package/WebDriverAgentLib/Utilities/FBConfiguration.m +8 -10
- package/WebDriverAgentLib/Utilities/FBSettings.h +1 -0
- package/WebDriverAgentLib/Utilities/FBSettings.m +1 -0
- package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +7 -0
- package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +9 -0
- package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +26 -18
- package/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +10 -7
- package/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +36 -15
- package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +4 -3
- package/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +0 -1
- package/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +34 -0
- package/package.json +1 -1
|
@@ -34,12 +34,23 @@ NSString *const FBDefaultApplicationAuto = @"auto";
|
|
|
34
34
|
|
|
35
35
|
NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
|
|
36
36
|
|
|
37
|
+
// FBXCAXClientProxy's shared accessibility channel can be stuck servicing another request.
|
|
38
|
+
static const NSTimeInterval FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC = 5.;
|
|
39
|
+
// -terminate hard-asserts off the main thread, which may itself be busy - see -fb_terminate...:.
|
|
40
|
+
static const NSTimeInterval FB_APP_TERMINATE_TIMEOUT_SEC = 5.;
|
|
41
|
+
// How long a -kill caller that lost the race below waits for the winner's teardown to finish.
|
|
42
|
+
static const NSTimeInterval FB_KILL_WAIT_TIMEOUT_SEC = 35.;
|
|
43
|
+
NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotification";
|
|
44
|
+
|
|
37
45
|
@interface FBSession ()
|
|
38
46
|
@property (nullable, nonatomic) XCUIApplication *testedApplication;
|
|
39
47
|
@property (nonatomic) BOOL isTestedApplicationExpectedToRun;
|
|
40
48
|
@property (nonatomic) BOOL shouldAppsWaitForQuiescence;
|
|
41
49
|
@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
|
|
42
50
|
@property (nonatomic, readwrite) NSMutableDictionary<NSNumber *, NSMutableDictionary<NSString *, NSNumber *> *> *elementsVisibilityCache;
|
|
51
|
+
|
|
52
|
+
- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout;
|
|
53
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout;
|
|
43
54
|
@end
|
|
44
55
|
|
|
45
56
|
@interface FBSession (FBAlertsMonitorDelegate)
|
|
@@ -90,17 +101,54 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
|
|
|
90
101
|
@implementation FBSession
|
|
91
102
|
|
|
92
103
|
static FBSession *_activeSession = nil;
|
|
104
|
+
// Class-level, not per-instance: a caller that finds _activeSession already nil (a concurrent
|
|
105
|
+
// -kill beat it there) still needs to know whether that -kill's teardown is done, since it cleared
|
|
106
|
+
// the pointer before running it. See +waitForActiveTeardownWithTimeout:.
|
|
107
|
+
static BOOL _isTeardownInProgress = NO;
|
|
108
|
+
|
|
109
|
+
+ (NSCondition *)teardownCondition
|
|
110
|
+
{
|
|
111
|
+
static NSCondition *condition;
|
|
112
|
+
static dispatch_once_t onceToken;
|
|
113
|
+
dispatch_once(&onceToken, ^{
|
|
114
|
+
condition = [NSCondition new];
|
|
115
|
+
});
|
|
116
|
+
return condition;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Waits (bounded) for any -kill teardown currently in progress to finish.
|
|
120
|
+
+ (void)waitForActiveTeardownWithTimeout:(NSTimeInterval)timeout
|
|
121
|
+
{
|
|
122
|
+
NSCondition *condition = self.teardownCondition;
|
|
123
|
+
[condition lock];
|
|
124
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
|
|
125
|
+
while (_isTeardownInProgress && [condition waitUntilDate:deadline]) {
|
|
126
|
+
}
|
|
127
|
+
[condition unlock];
|
|
128
|
+
}
|
|
93
129
|
|
|
94
130
|
+ (instancetype)activeSession
|
|
95
131
|
{
|
|
96
132
|
return _activeSession;
|
|
97
133
|
}
|
|
98
134
|
|
|
99
|
-
+ (void)
|
|
135
|
+
+ (void)killActiveSessionAndWaitForTeardown
|
|
100
136
|
{
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
FBSession *session = _activeSession;
|
|
138
|
+
if (nil != session) {
|
|
139
|
+
// Runs the real teardown synchronously if this call wins the race in -kill, or waits for
|
|
140
|
+
// whoever did to finish if it lost - either way, blocks until torn down.
|
|
141
|
+
[session kill];
|
|
142
|
+
} else {
|
|
143
|
+
// _activeSession is already nil, but a concurrent -kill (e.g. from DELETE /session) may still
|
|
144
|
+
// be mid-teardown - wait for it, so we don't launch a replacement app too early.
|
|
145
|
+
[self waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC];
|
|
103
146
|
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
+ (void)markSessionActive:(FBSession *)session
|
|
150
|
+
{
|
|
151
|
+
[self killActiveSessionAndWaitForTeardown];
|
|
104
152
|
_activeSession = session;
|
|
105
153
|
}
|
|
106
154
|
|
|
@@ -169,33 +217,58 @@ static FBSession *_activeSession = nil;
|
|
|
169
217
|
|
|
170
218
|
- (void)kill
|
|
171
219
|
{
|
|
172
|
-
|
|
220
|
+
// DELETE /session and session creation can now run concurrently, so a session already
|
|
221
|
+
// superseded by a newer one can still reach here via a stale reference. Check-and-clear must be
|
|
222
|
+
// atomic, else a belated -kill could null out the new session's pointer instead of its own.
|
|
223
|
+
BOOL wasActive;
|
|
224
|
+
@synchronized (self.class) {
|
|
225
|
+
wasActive = (self == _activeSession);
|
|
226
|
+
if (wasActive) {
|
|
227
|
+
_activeSession = nil;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!wasActive) {
|
|
231
|
+
// Someone else is already tearing this session down - wait for that to finish (bounded), so
|
|
232
|
+
// we don't act as if it's gone (e.g. launch a new app) while its -terminate is still in flight.
|
|
233
|
+
[self.class waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC];
|
|
173
234
|
return;
|
|
174
235
|
}
|
|
175
236
|
|
|
176
|
-
|
|
237
|
+
NSCondition *teardownCondition = self.class.teardownCondition;
|
|
238
|
+
[teardownCondition lock];
|
|
239
|
+
_isTeardownInProgress = YES;
|
|
240
|
+
[teardownCondition unlock];
|
|
241
|
+
|
|
242
|
+
@try {
|
|
243
|
+
// Posted before teardown so pending HTTP requests for this session can stop waiting sooner.
|
|
244
|
+
[NSNotificationCenter.defaultCenter postNotificationName:FBSessionWasKilledNotification object:self];
|
|
177
245
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
246
|
+
[self disableAlertsMonitor];
|
|
247
|
+
|
|
248
|
+
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
|
|
249
|
+
if (nil != activeScreenRecording) {
|
|
250
|
+
NSError *error;
|
|
251
|
+
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) {
|
|
252
|
+
[FBLogger logFmt:@"%@", error];
|
|
253
|
+
}
|
|
254
|
+
[FBScreenRecordingContainer.sharedInstance reset];
|
|
183
255
|
}
|
|
184
|
-
[FBScreenRecordingContainer.sharedInstance reset];
|
|
185
|
-
}
|
|
186
256
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
[
|
|
257
|
+
if (nil != self.testedApplication
|
|
258
|
+
&& FBConfiguration.sharedInstance.shouldTerminateApp
|
|
259
|
+
&& self.testedApplication.running
|
|
260
|
+
&& ![self fb_isTestedApplicationSameAsSystemAppWithTimeout:FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC]) {
|
|
261
|
+
// Blocks until the app is either actually terminated or durably given up on (never left
|
|
262
|
+
// pending) - see -fb_terminateTestedApplicationWithTimeout: - so it's safe to report this
|
|
263
|
+
// teardown as finished as soon as this returns.
|
|
264
|
+
[self fb_terminateTestedApplicationWithTimeout:FB_APP_TERMINATE_TIMEOUT_SEC];
|
|
195
265
|
}
|
|
266
|
+
} @finally {
|
|
267
|
+
[teardownCondition lock];
|
|
268
|
+
_isTeardownInProgress = NO;
|
|
269
|
+
[teardownCondition broadcast];
|
|
270
|
+
[teardownCondition unlock];
|
|
196
271
|
}
|
|
197
|
-
|
|
198
|
-
_activeSession = nil;
|
|
199
272
|
}
|
|
200
273
|
|
|
201
274
|
- (XCUIApplication *)activeApplication
|
|
@@ -290,4 +363,62 @@ static FBSession *_activeSession = nil;
|
|
|
290
363
|
: [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier];
|
|
291
364
|
}
|
|
292
365
|
|
|
366
|
+
// Has no async variant and can block on the shared accessibility channel. Run off-thread and give
|
|
367
|
+
// up after `timeout`, assuming the tested app IS the system app - safer, since it means skipping
|
|
368
|
+
// termination rather than risking terminating springboard.
|
|
369
|
+
- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout
|
|
370
|
+
{
|
|
371
|
+
__block XCUIApplication *systemApp = nil;
|
|
372
|
+
__block NSException *caughtException = nil;
|
|
373
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
374
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
375
|
+
// Undocumented private API; guard in case it hard-asserts off-main like -terminate does on
|
|
376
|
+
// some Xcode/iOS version - uncaught, that would crash the whole process.
|
|
377
|
+
@try {
|
|
378
|
+
systemApp = XCUIApplication.fb_systemApplication;
|
|
379
|
+
} @catch (NSException *e) {
|
|
380
|
+
caughtException = e;
|
|
381
|
+
}
|
|
382
|
+
dispatch_semaphore_signal(sem);
|
|
383
|
+
});
|
|
384
|
+
int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
|
|
385
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) || nil != caughtException) {
|
|
386
|
+
[FBLogger logFmt:@"Could not determine the system application within %@ seconds%@; assuming '%@' might be it and skipping its termination", @(timeout), nil == caughtException ? @"" : [NSString stringWithFormat:@" (%@)", caughtException.description], self.testedApplication.bundleID];
|
|
387
|
+
return YES;
|
|
388
|
+
}
|
|
389
|
+
return [self.testedApplication fb_isSameAppAs:systemApp];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// -terminate hard-asserts off-main, but -kill can now run on a background queue. Dispatching to
|
|
393
|
+
// main and waiting indefinitely could hang just as long as main is stuck, so give up after
|
|
394
|
+
// `timeout` - but a "given up on" call must never still terminate whatever's running by the time
|
|
395
|
+
// main gets to it (e.g. a replacement session's app), so cancellation and the actual terminate
|
|
396
|
+
// call share a lock: whichever gets there first - the dispatched block, or the timeout - wins.
|
|
397
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout
|
|
398
|
+
{
|
|
399
|
+
XCUIApplication *application = self.testedApplication;
|
|
400
|
+
NSObject *lock = [NSObject new];
|
|
401
|
+
__block BOOL isAllowedToTerminate = YES;
|
|
402
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
403
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
404
|
+
@synchronized (lock) {
|
|
405
|
+
if (isAllowedToTerminate) {
|
|
406
|
+
@try {
|
|
407
|
+
[application terminate];
|
|
408
|
+
} @catch (NSException *e) {
|
|
409
|
+
[FBLogger logFmt:@"%@", e.description];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
dispatch_semaphore_signal(sem);
|
|
414
|
+
});
|
|
415
|
+
int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
|
|
416
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
|
417
|
+
@synchronized (lock) {
|
|
418
|
+
isAllowedToTerminate = NO;
|
|
419
|
+
}
|
|
420
|
+
[FBLogger logFmt:@"Could not terminate '%@' within %@ seconds; giving up on it rather than risk terminating a possible replacement session's app later", application.bundleID, @(timeout)];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
293
424
|
@end
|
|
@@ -135,7 +135,8 @@
|
|
|
135
135
|
}
|
|
136
136
|
[strongSelf scheduleReceiveForConnection:connection];
|
|
137
137
|
} else if (nw_connection_state_failed == state || nw_connection_state_cancelled == state) {
|
|
138
|
-
|
|
138
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
139
|
+
[strongSelf handleDisconnectForConnection:connection];
|
|
139
140
|
}
|
|
140
141
|
});
|
|
141
142
|
nw_connection_start(connection);
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
#import "FBTCPSocket.h"
|
|
14
14
|
|
|
15
15
|
#import "FBCommandHandler.h"
|
|
16
|
+
#import "FBCommandStatus.h"
|
|
16
17
|
#import "FBErrorBuilder.h"
|
|
17
18
|
#import "FBExceptionHandler.h"
|
|
19
|
+
#import "FBResponsePayload.h"
|
|
18
20
|
#import "FBRouteRequest.h"
|
|
19
21
|
#import "FBRuntimeUtils.h"
|
|
20
22
|
#import "FBSession.h"
|
|
@@ -80,6 +82,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
80
82
|
[self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"];
|
|
81
83
|
[self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"];
|
|
82
84
|
|
|
85
|
+
[NSNotificationCenter.defaultCenter addObserver:self
|
|
86
|
+
selector:@selector(sessionWasKilled:)
|
|
87
|
+
name:FBSessionWasKilledNotification
|
|
88
|
+
object:nil];
|
|
89
|
+
|
|
83
90
|
[self registerRouteHandlers:[self.class collectCommandHandlerClasses]];
|
|
84
91
|
[self registerServerKeyRouteHandlers];
|
|
85
92
|
|
|
@@ -167,8 +174,26 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
- (void)sessionWasKilled:(NSNotification *)notification
|
|
178
|
+
{
|
|
179
|
+
FBSession *session = notification.object;
|
|
180
|
+
if (![session isKindOfClass:FBSession.class]) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Same "invalid session id" shape a still-queued request would eventually get anyway, once
|
|
184
|
+
// -routeQueue drains and FBRoute.decorateRequest: finds the session gone - just delivered now
|
|
185
|
+
// instead of after however long the request would otherwise have been stuck waiting.
|
|
186
|
+
NSString *message = [NSString stringWithFormat:@"Session %@ was deleted while this request was still pending", session.identifier];
|
|
187
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus noSuchDriverErrorWithMessage:message
|
|
188
|
+
traceback:nil]);
|
|
189
|
+
RouteResponse *response = [RouteResponse new];
|
|
190
|
+
[payload dispatchWithResponse:response];
|
|
191
|
+
[self.server abandonPendingRequestsForSessionID:session.identifier withResponse:response];
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
- (void)stopServing
|
|
171
195
|
{
|
|
196
|
+
[NSNotificationCenter.defaultCenter removeObserver:self name:FBSessionWasKilledNotification object:nil];
|
|
172
197
|
[FBSession.activeSession kill];
|
|
173
198
|
[self stopScreenshotsBroadcaster];
|
|
174
199
|
if (self.server.isRunning) {
|
|
@@ -208,7 +233,7 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
208
233
|
for (Class<FBCommandHandler> commandHandler in commandHandlerClasses) {
|
|
209
234
|
NSArray *routes = [commandHandler routes];
|
|
210
235
|
for (FBRoute *route in routes) {
|
|
211
|
-
[self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) {
|
|
236
|
+
[self.server handleMethod:route.verb withPath:route.path standalone:route.isStandalone block:^(RouteRequest *request, RouteResponse *response) {
|
|
212
237
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
213
238
|
if (nil == strongSelf) {
|
|
214
239
|
return;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#import "RouteResponse.h"
|
|
10
10
|
|
|
11
11
|
@interface RouteResponse ()
|
|
12
|
-
@property (nonatomic,
|
|
12
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *mutableHeaders;
|
|
13
13
|
@end
|
|
14
14
|
|
|
15
15
|
@implementation RouteResponse
|
|
@@ -234,6 +234,17 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
|
|
|
234
234
|
*/
|
|
235
235
|
@property (atomic, assign) NSTimeInterval animationCoolOffTimeout;
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Maximum time to wait for the frontmost application to confirm its main run loop
|
|
239
|
+
* is responsive before an accessibility snapshot request (element attribute
|
|
240
|
+
* lookups, active app detection, etc). XCTest has no bounded timeout of its own
|
|
241
|
+
* here, so a frozen app could otherwise block WDA forever (#1210); past this
|
|
242
|
+
* timeout the request is aborted with an error instead.
|
|
243
|
+
* Set to zero or negative to disable, restoring unbounded behavior. Disabled (0)
|
|
244
|
+
* by default.
|
|
245
|
+
*/
|
|
246
|
+
@property (atomic, assign) NSTimeInterval accessibilityDeadline;
|
|
247
|
+
|
|
237
248
|
/**
|
|
238
249
|
Custom class chain locator for accept alert button location.
|
|
239
250
|
This might be useful if the default buttons detection algorithm fails to determine alert buttons properly
|
|
@@ -124,17 +124,14 @@ static NSString *const axSettingsClassName = @"AXSettings";
|
|
|
124
124
|
{
|
|
125
125
|
// 'WebDriverAgent --port 8080' can be passed via the arguments to the process
|
|
126
126
|
NSRange rangeFromArguments = [self.class bindingPortRangeFromArguments];
|
|
127
|
-
if (rangeFromArguments.location
|
|
128
|
-
|
|
127
|
+
if (rangeFromArguments.location == NSNotFound) {
|
|
128
|
+
// Existence of USE_PORT in the environment implies the port range is managed by the launching process.
|
|
129
|
+
NSString *usePort = NSProcessInfo.processInfo.environment[@"USE_PORT"];
|
|
130
|
+
rangeFromArguments = usePort.length > 0
|
|
131
|
+
? NSMakeRange((NSUInteger)usePort.integerValue, 1)
|
|
132
|
+
: NSMakeRange(DefaultStartingPort, DefaultPortRange);
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
// Existence of USE_PORT in the environment implies the port range is managed by the launching process.
|
|
132
|
-
if (NSProcessInfo.processInfo.environment[@"USE_PORT"] &&
|
|
133
|
-
[NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) {
|
|
134
|
-
return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return NSMakeRange(DefaultStartingPort, DefaultPortRange);
|
|
134
|
+
return rangeFromArguments;
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
- (NSString *)bindingIPAddress
|
|
@@ -354,6 +351,7 @@ static NSString *const axSettingsClassName = @"AXSettings";
|
|
|
354
351
|
self.autoClickAlertSelector = @"";
|
|
355
352
|
self.waitForIdleTimeout = 10.;
|
|
356
353
|
self.animationCoolOffTimeout = 2.;
|
|
354
|
+
self.accessibilityDeadline = 0.;
|
|
357
355
|
// 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100.
|
|
358
356
|
FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50);
|
|
359
357
|
FBSetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey, @INT_MAX);
|
|
@@ -23,6 +23,7 @@ extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION;
|
|
|
23
23
|
extern NSString* const FB_SETTING_KEYBOARD_PREDICTION;
|
|
24
24
|
extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH;
|
|
25
25
|
extern NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN;
|
|
26
|
+
extern NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE;
|
|
26
27
|
extern NSString* const FB_SETTING_USE_FIRST_MATCH;
|
|
27
28
|
extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX;
|
|
28
29
|
extern NSString* const FB_SETTING_REDUCE_MOTION;
|
|
@@ -19,6 +19,7 @@ NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection";
|
|
|
19
19
|
NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction";
|
|
20
20
|
NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth";
|
|
21
21
|
NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN = @"snapshotMaxChildren";
|
|
22
|
+
NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE = @"accessibilityDeadline";
|
|
22
23
|
NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch";
|
|
23
24
|
NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex";
|
|
24
25
|
NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion";
|
|
@@ -142,6 +142,10 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
|
|
|
142
142
|
FBConfiguration.sharedInstance.animationCoolOffTimeout = [value doubleValue];
|
|
143
143
|
return nil;
|
|
144
144
|
};
|
|
145
|
+
map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^FBCommandStatus *(FBSession *session, id value) {
|
|
146
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = [value doubleValue];
|
|
147
|
+
return nil;
|
|
148
|
+
};
|
|
145
149
|
map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^FBCommandStatus *(FBSession *session, id value) {
|
|
146
150
|
if (nil == value) {
|
|
147
151
|
session.defaultAlertAction = nil;
|
|
@@ -249,6 +253,9 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
|
|
|
249
253
|
map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^id(FBSession *session) {
|
|
250
254
|
return @(FBConfiguration.sharedInstance.animationCoolOffTimeout);
|
|
251
255
|
};
|
|
256
|
+
map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^id(FBSession *session) {
|
|
257
|
+
return @(FBConfiguration.sharedInstance.accessibilityDeadline);
|
|
258
|
+
};
|
|
252
259
|
map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^id(FBSession *session) {
|
|
253
260
|
return @(FBConfiguration.sharedInstance.boundElementsByIndex);
|
|
254
261
|
};
|
|
@@ -38,6 +38,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
38
38
|
- (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application
|
|
39
39
|
reply:(void (^)(void))reply;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
Wraps the private -[XCAXClient_iOS notifyWhenEventLoopIsIdleForApplication:reply:],
|
|
43
|
+
used to check run loop responsiveness before a snapshot request (#1210).
|
|
44
|
+
`reply` may fire more than once per call; `error` is non-nil only if monitoring
|
|
45
|
+
itself could not be started.
|
|
46
|
+
*/
|
|
47
|
+
- (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application
|
|
48
|
+
reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply;
|
|
49
|
+
|
|
41
50
|
- (nullable NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
|
|
42
51
|
attributes:(NSArray *)attributes
|
|
43
52
|
error:(NSError**)error;
|
|
@@ -81,6 +81,12 @@ static id FBAXClient = nil;
|
|
|
81
81
|
[FBAXClient notifyWhenNoAnimationsAreActiveForApplication:application reply:reply];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
- (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application
|
|
85
|
+
reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply
|
|
86
|
+
{
|
|
87
|
+
[FBAXClient notifyWhenEventLoopIsIdleForApplication:application reply:reply];
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
- (NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
|
|
85
91
|
attributes:(NSArray *)attributes
|
|
86
92
|
error:(NSError**)error;
|
|
@@ -92,28 +98,30 @@ static id FBAXClient = nil;
|
|
|
92
98
|
|
|
93
99
|
- (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid
|
|
94
100
|
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
[
|
|
101
|
+
@synchronized (self) {
|
|
102
|
+
NSMutableSet *terminatedAppIds = [NSMutableSet set];
|
|
103
|
+
for (NSNumber *appPid in self.appsCache) {
|
|
104
|
+
if (![self.appsCache[appPid] running]) {
|
|
105
|
+
[terminatedAppIds addObject:appPid];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (NSNumber *appPid in terminatedAppIds) {
|
|
109
|
+
[self.appsCache removeObjectForKey:appPid];
|
|
99
110
|
}
|
|
100
|
-
}
|
|
101
|
-
for (NSNumber *appPid in terminatedAppIds) {
|
|
102
|
-
[self.appsCache removeObjectForKey:appPid];
|
|
103
|
-
}
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
XCUIApplication *result = [self.appsCache objectForKey:@(pid)];
|
|
113
|
+
if (nil != result) {
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
XCUIApplication *app = [[FBAXClient applicationProcessTracker]
|
|
118
|
+
monitoredApplicationWithProcessIdentifier:pid];
|
|
119
|
+
if (nil == app) {
|
|
120
|
+
return nil;
|
|
121
|
+
}
|
|
122
|
+
[self.appsCache setObject:app forKey:@(pid)];
|
|
123
|
+
return app;
|
|
114
124
|
}
|
|
115
|
-
[self.appsCache setObject:app forKey:@(pid)];
|
|
116
|
-
return app;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
@end
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
#import "XCUIDevice.h"
|
|
24
24
|
|
|
25
25
|
#define LAUNCH_APP_TIMEOUT_SEC 300
|
|
26
|
+
#define STOP_SCREEN_RECORDING_TIMEOUT_SEC 20
|
|
26
27
|
|
|
27
28
|
static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));
|
|
28
29
|
|
|
@@ -342,14 +343,16 @@ static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundl
|
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
__block NSError *innerError = nil;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
completion();
|
|
351
|
-
}];
|
|
346
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
347
|
+
[session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) {
|
|
348
|
+
innerError = invokeError;
|
|
349
|
+
dispatch_semaphore_signal(sem);
|
|
352
350
|
}];
|
|
351
|
+
int64_t timeoutNs = (int64_t)(STOP_SCREEN_RECORDING_TIMEOUT_SEC * NSEC_PER_SEC);
|
|
352
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) && nil == innerError) {
|
|
353
|
+
NSString *message = [NSString stringWithFormat:@"Did not receive a reply to stop screen recording within %d seconds", STOP_SCREEN_RECORDING_TIMEOUT_SEC];
|
|
354
|
+
innerError = [[[FBErrorBuilder builder] withDescription:message] build];
|
|
355
|
+
}
|
|
353
356
|
if (nil != innerError && error) {
|
|
354
357
|
*error = innerError;
|
|
355
358
|
}
|
|
@@ -77,32 +77,53 @@
|
|
|
77
77
|
|
|
78
78
|
@end
|
|
79
79
|
|
|
80
|
+
#define TESTMANAGERD_VERSION_TIMEOUT_SEC 20
|
|
81
|
+
|
|
80
82
|
NSInteger FBTestmanagerdVersion(void)
|
|
81
83
|
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
// Not dispatch_once: that would permanently cache the timeout fallback below if the first call's
|
|
85
|
+
// reply merely arrived late. -1 means "not yet determined"; a timeout isn't cached, so it retries.
|
|
86
|
+
static NSInteger cachedVersion = -1;
|
|
87
|
+
static dispatch_queue_t syncQueue;
|
|
88
|
+
static dispatch_once_t onceToken;
|
|
89
|
+
dispatch_once(&onceToken, ^{
|
|
90
|
+
syncQueue = dispatch_queue_create("com.facebook.wda.testmanagerdVersion", DISPATCH_QUEUE_SERIAL);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
__block NSInteger result;
|
|
94
|
+
dispatch_sync(syncQueue, ^{
|
|
95
|
+
if (cachedVersion >= 0) {
|
|
96
|
+
result = cachedVersion;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
id<XCTMessagingChannel_RunnerToDaemon> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
|
86
101
|
if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) {
|
|
87
102
|
id<FBXCTestManagerLegacyProtocolVersionExchanging> legacyProxy = (id<FBXCTestManagerLegacyProtocolVersionExchanging>)proxy;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
__block NSInteger receivedVersion = -1;
|
|
104
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
105
|
+
[legacyProxy _XCT_exchangeProtocolVersion:0 reply:^(unsigned long long code) {
|
|
106
|
+
receivedVersion = (NSInteger) code;
|
|
107
|
+
dispatch_semaphore_signal(sem);
|
|
93
108
|
}];
|
|
109
|
+
int64_t timeoutNs = (int64_t)(TESTMANAGERD_VERSION_TIMEOUT_SEC * NSEC_PER_SEC);
|
|
110
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
|
111
|
+
// Assume newest/full-featured on timeout, but don't cache it - retry on the next call.
|
|
112
|
+
[FBLogger logFmt:@"Did not receive a testmanagerd protocol version reply within %d seconds; assuming the newest/full-featured protocol", TESTMANAGERD_VERSION_TIMEOUT_SEC];
|
|
113
|
+
result = 0xFFFF;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
result = receivedVersion;
|
|
94
117
|
} else {
|
|
95
|
-
// Modern testmanagerd (Xcode 15+)
|
|
96
|
-
//
|
|
97
|
-
// integer equivalent to report here (this value is diagnostic-only, surfaced via the
|
|
98
|
-
// 'testmanagerdVersion' session capability), so keep reporting the existing "assume
|
|
99
|
-
// newest/full-featured" sentinel, while confirming capabilities did negotiate successfully.
|
|
118
|
+
// Modern testmanagerd (Xcode 15+) negotiates named XCTCapabilities instead of a scalar
|
|
119
|
+
// version; there's no direct integer equivalent, so just confirm capabilities negotiated.
|
|
100
120
|
XCTCapabilities *capabilities = [XCTRunnerDaemonSession sharedSession].remoteInterfaceCapabilities;
|
|
101
121
|
if (nil == capabilities) {
|
|
102
122
|
[FBLogger log:@"Could not retrieve testmanagerd capabilities"];
|
|
103
123
|
}
|
|
104
|
-
|
|
124
|
+
result = 0xFFFF;
|
|
105
125
|
}
|
|
126
|
+
cachedVersion = result;
|
|
106
127
|
});
|
|
107
|
-
return
|
|
128
|
+
return result;
|
|
108
129
|
}
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
|
|
39
39
|
- (IBAction)deadlockApp:(id)sender
|
|
40
40
|
{
|
|
41
|
-
dispatch_sync
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// A self dispatch_sync would trip the OS watchdog and get the process
|
|
42
|
+
// killed outright. Sleeping instead simulates an app that stops answering
|
|
43
|
+
// accessibility requests while staying alive, per #1210.
|
|
44
|
+
[NSThread sleepForTimeInterval:20.0];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
- (IBAction)didTapButton:(UIButton *)button
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
<state key="normal" title="Deadlock app"/>
|
|
39
39
|
<connections>
|
|
40
40
|
<action selector="deadlockApp:" destination="BYZ-38-t0r" eventType="touchUpInside" id="53X-DJ-KNY"/>
|
|
41
|
-
<action selector="showAlert:" destination="BYZ-38-t0r" eventType="touchUpInside" id="FEN-VX-MMc"/>
|
|
42
41
|
</connections>
|
|
43
42
|
</button>
|
|
44
43
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M2N-Yn-ytb">
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
#import "FBConfiguration.h"
|
|
13
13
|
#import "FBRuntimeUtils.h"
|
|
14
|
+
#import "FBTestMacros.h"
|
|
15
|
+
#import "XCUIElement.h"
|
|
16
|
+
#import "XCUIElement+FBIsVisible.h"
|
|
14
17
|
|
|
15
18
|
@interface FBConfigurationTests : FBIntegrationTestCase
|
|
16
19
|
|
|
@@ -35,4 +38,35 @@
|
|
|
35
38
|
XCTAssertEqual(FBConfiguration.sharedInstance.reduceMotionEnabled, defaultReduceMotionEnabled);
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
- (void)testAccessibilityDeadlineAbortsSnapshotRequestForDeadlockedApp
|
|
42
|
+
{
|
|
43
|
+
if (nil != NSProcessInfo.processInfo.environment[@"CI"]) {
|
|
44
|
+
XCTSkip(@"Deliberately freezes the app for several seconds, too slow/flaky for CI");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
NSTimeInterval previousDeadline = FBConfiguration.sharedInstance.accessibilityDeadline;
|
|
48
|
+
// Also bounds any snapshot-based wait -tap itself may perform once the app is stuck.
|
|
49
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = 3.0;
|
|
50
|
+
@try {
|
|
51
|
+
XCUIElement *deadlockButton = self.testedApplication.buttons[@"Deadlock app"];
|
|
52
|
+
FBAssertWaitTillBecomesTrue(deadlockButton.fb_isVisible);
|
|
53
|
+
// Freezes the app's main thread for 20s - see -[ViewController deadlockApp:].
|
|
54
|
+
[deadlockButton tap];
|
|
55
|
+
|
|
56
|
+
NSError *error;
|
|
57
|
+
NSDate *start = [NSDate date];
|
|
58
|
+
id snapshot = [self.testedApplication snapshotWithError:&error];
|
|
59
|
+
NSTimeInterval elapsed = -start.timeIntervalSinceNow;
|
|
60
|
+
|
|
61
|
+
XCTAssertNil(snapshot);
|
|
62
|
+
XCTAssertNotNil(error);
|
|
63
|
+
// Should abort close to accessibilityDeadline (plus XCTest's own internal
|
|
64
|
+
// retries), not hang indefinitely waiting for the frozen app (#1210).
|
|
65
|
+
XCTAssertLessThan(elapsed, 20.0);
|
|
66
|
+
} @finally {
|
|
67
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = previousDeadline;
|
|
68
|
+
[self.testedApplication terminate];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
@end
|