appium-webdriveragent 16.12.11 → 16.13.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h +53 -0
  3. package/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +124 -0
  4. package/WebDriverAgentLib/Commands/FBElementCommands.m +20 -5
  5. package/WebDriverAgentLib/Info.plist +2 -2
  6. package/WebDriverAgentLib/Routing/FBHTTPServer.m +14 -1
  7. package/WebDriverAgentLib/Routing/FBTCPSocket.h +5 -1
  8. package/WebDriverAgentLib/Routing/FBTCPSocket.m +10 -1
  9. package/WebDriverAgentLib/Utilities/FBConfiguration.h +6 -0
  10. package/WebDriverAgentLib/Utilities/FBConfiguration.m +1 -0
  11. package/WebDriverAgentLib/Utilities/FBImageProcessor.m +3 -1
  12. package/WebDriverAgentLib/Utilities/FBMjpegServer.m +2 -0
  13. package/WebDriverAgentLib/Utilities/FBScreen.h +17 -0
  14. package/WebDriverAgentLib/Utilities/FBScreen.m +32 -0
  15. package/WebDriverAgentLib/Utilities/FBScreenshot.h +2 -2
  16. package/WebDriverAgentLib/Utilities/FBScreenshot.m +6 -2
  17. package/WebDriverAgentLib/Utilities/FBSettings.h +1 -0
  18. package/WebDriverAgentLib/Utilities/FBSettings.m +1 -0
  19. package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +24 -0
  20. package/WebDriverAgentLib/Utilities/FBXPath.m +36 -4
  21. package/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m +8 -5
  22. package/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h +1 -1
  23. package/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m +95 -0
  24. package/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m +34 -0
  25. package/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +67 -0
  26. package/WebDriverAgentTests/UnitTests/FBHTTPServerTests.m +149 -5
  27. package/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m +34 -0
  28. package/WebDriverAgentTests/UnitTests/FBXPathTests.m +36 -0
  29. package/build/lib/xcodebuild.d.ts +0 -1
  30. package/build/lib/xcodebuild.d.ts.map +1 -1
  31. package/build/lib/xcodebuild.js +7 -7
  32. package/build/lib/xcodebuild.js.map +1 -1
  33. package/lib/xcodebuild.ts +7 -7
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [16.13.1](https://github.com/appium/WebDriverAgent/compare/v16.13.0...v16.13.1) (2026-09-30)
2
+
3
+ ### Performance Improvements
4
+
5
+ * reuse sibling indices and geometry during XML serialization ([#1283](https://github.com/appium/WebDriverAgent/issues/1283)) ([f352660](https://github.com/appium/WebDriverAgent/commit/f352660deb52728c195a875b4172e38e1a1496b8))
6
+
7
+ ## [16.13.0](https://github.com/appium/WebDriverAgent/compare/v16.12.11...v16.13.0) (2026-09-30)
8
+
9
+ ### Features
10
+
11
+ * add currentDisplayId setting for screenshots ([#1267](https://github.com/appium/WebDriverAgent/issues/1267)) ([c9366a2](https://github.com/appium/WebDriverAgent/commit/c9366a227b6de19381c96d56a548e9fb53d77dba))
12
+
13
+ ### Bug Fixes
14
+
15
+ * apply screenshot scaling once when correcting orientation ([#1278](https://github.com/appium/WebDriverAgent/issues/1278)) ([17fd4d0](https://github.com/appium/WebDriverAgent/commit/17fd4d059c351d73384604be40bbade4a03be9a9))
16
+ * avoid infinite-point assertion and hang in coordinate drag gestures ([#1270](https://github.com/appium/WebDriverAgent/issues/1270)) ([c9290a0](https://github.com/appium/WebDriverAgent/commit/c9290a01a63fa195c8aba35c9f0cdad1cd9a6d64)), closes [#1056](https://github.com/appium/WebDriverAgent/issues/1056)
17
+ * cancel connections that end on the client side ([#1273](https://github.com/appium/WebDriverAgent/issues/1273)) ([261c08d](https://github.com/appium/WebDriverAgent/commit/261c08d53917b138eaf2f7d8a744d1151fe3bc8d))
18
+ * isolate xcodebuild readiness state between launches ([#1275](https://github.com/appium/WebDriverAgent/issues/1275)) ([ccc46fe](https://github.com/appium/WebDriverAgent/commit/ccc46fe6de1871a020205488533c1d5b9b7dc756))
19
+ * release LRU cache values when the cache is destroyed ([#1274](https://github.com/appium/WebDriverAgent/issues/1274)) ([fb52865](https://github.com/appium/WebDriverAgent/commit/fb5286515584157659ed86bccdbb64bc3094d91f))
20
+
1
21
  ## [16.12.11](https://github.com/appium/WebDriverAgent/compare/v16.12.10...v16.12.11) (2026-09-28)
2
22
 
3
23
  ### Bug Fixes
@@ -10,6 +10,8 @@
10
10
  #import <XCTest/XCTest.h>
11
11
  #import "FBElementCache.h"
12
12
 
13
+ @class XCSynthesizedEventRecord;
14
+
13
15
  NS_ASSUME_NONNULL_BEGIN
14
16
 
15
17
  @interface XCUIApplication (FBTouchAction)
@@ -24,6 +26,57 @@ NS_ASSUME_NONNULL_BEGIN
24
26
  */
25
27
  - (BOOL)fb_performW3CActions:(NSArray *)actions elementCache:(nullable FBElementCache *)elementCache error:(NSError * _Nullable*)error;
26
28
 
29
+ /**
30
+ Presses at startCoordinate, drags to endCoordinate and holds there, producing the same touch event
31
+ path as -[XCUICoordinate pressForDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:].
32
+
33
+ Unlike that XCTest method, both screen points are resolved exactly once, up front, and validated
34
+ before anything is sent; the resulting event is then synthesized directly, without XCTest's
35
+ dispatch-time re-snapshot and quiescence wait of the application. XCTest resolves the points
36
+ inside that dispatch, so an application that is busy (e.g. right after an app switch) makes it
37
+ hang and then assert on an infinite point instead of failing cleanly.
38
+ See https://github.com/appium/WebDriverAgent/issues/1056
39
+
40
+ @param startCoordinate The coordinate to press at
41
+ @param pressDuration How long to hold still at startCoordinate before dragging, in seconds (>= 0)
42
+ @param endCoordinate The coordinate to drag to
43
+ @param velocity Drag velocity in points per second, or one of the XCUIGestureVelocity constants
44
+ @param holdDuration How long to hold still at endCoordinate before lifting, in seconds (>= 0)
45
+ @param error If there is an error, upon return contains an NSError object that describes the problem
46
+ @return YES if the gesture has been successfully performed
47
+ @throws FBInvalidArgumentException if a duration or the velocity is out of range
48
+ */
49
+ - (BOOL)fb_pressAtCoordinate:(XCUICoordinate *)startCoordinate
50
+ forDuration:(NSTimeInterval)pressDuration
51
+ thenDragToCoordinate:(XCUICoordinate *)endCoordinate
52
+ withVelocity:(XCUIGestureVelocity)velocity
53
+ thenHoldForDuration:(NSTimeInterval)holdDuration
54
+ error:(NSError * _Nullable*)error;
55
+
56
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
57
+ /**
58
+ Builds the synthesized event used by
59
+ -fb_pressAtCoordinate:forDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:error:
60
+ from already resolved screen points. Exposed for testing.
61
+
62
+ @param startPoint The screen point to press at
63
+ @param pressDuration How long to hold still at startPoint before dragging, in seconds
64
+ @param endPoint The screen point to drag to
65
+ @param pointsPerSecond The drag speed
66
+ @param holdDuration How long to hold still at endPoint before lifting, in seconds
67
+ @param displayID The display to send the gesture to (0 is the main display)
68
+ @param interfaceOrientation The interface orientation the points are expressed in
69
+ @return The event record, ready to be synthesized
70
+ */
71
+ + (XCSynthesizedEventRecord *)fb_pressDragEventFromPoint:(CGPoint)startPoint
72
+ forDuration:(NSTimeInterval)pressDuration
73
+ toPoint:(CGPoint)endPoint
74
+ pointsPerSecond:(CGFloat)pointsPerSecond
75
+ thenHoldForDuration:(NSTimeInterval)holdDuration
76
+ displayID:(long long)displayID
77
+ interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
78
+ #endif
79
+
27
80
  @end
28
81
 
29
82
  NS_ASSUME_NONNULL_END
@@ -9,17 +9,67 @@
9
9
 
10
10
  #import "XCUIApplication+FBTouchAction.h"
11
11
 
12
+ #import <dlfcn.h>
13
+
12
14
  #import "FBBaseActionsSynthesizer.h"
13
15
  #import "FBConfiguration.h"
16
+ #import "FBErrorBuilder.h"
14
17
  #import "FBExceptions.h"
15
18
  #import "FBLogger.h"
19
+ #import "FBMathUtils.h"
16
20
  #import "FBRunLoopSpinner.h"
17
21
  #import "FBW3CActionsSynthesizer.h"
18
22
  #import "FBXCTestDaemonsProxy.h"
23
+ #import "XCPointerEventPath.h"
24
+ #import "XCSynthesizedEventRecord.h"
25
+ #import "XCUIElement.h"
19
26
  #import "XCUIElement+FBUtilities.h"
20
27
 
21
28
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
22
29
 
30
+ /**
31
+ Maps a drag XCUIGestureVelocity to the speed XCTest drags at. XCTest calls the result "pixels per
32
+ second", but divides the distance between the two screen points (which are in points) by it.
33
+ Prefers XCTest's own (exported, but undeclared) mapping function, so any future change to its
34
+ presets is picked up; falls back to the values it returns now.
35
+
36
+ @throws FBInvalidArgumentException if the velocity is neither one of the XCUIGestureVelocity
37
+ presets nor a positive finite number
38
+ */
39
+ static CGFloat FBPointsPerSecondForDragVelocity(XCUIGestureVelocity velocity)
40
+ {
41
+ BOOL isDefault = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocityDefault, 0);
42
+ BOOL isSlow = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocitySlow, 0);
43
+ BOOL isFast = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocityFast, 0);
44
+ if (!isDefault && !isSlow && !isFast && !(isfinite(velocity) && velocity > 0)) {
45
+ NSString *reason = [NSString stringWithFormat:@"%@ is an invalid drag velocity. It must be greater than 0", @(velocity)];
46
+ @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:nil];
47
+ }
48
+ static double (*xctestMapping)(double) = NULL;
49
+ static dispatch_once_t onceToken;
50
+ dispatch_once(&onceToken, ^{
51
+ xctestMapping = (double (*)(double))dlsym(RTLD_DEFAULT, "XCUIPixelsPerSecondForDragGestureVelocity");
52
+ if (NULL == xctestMapping) {
53
+ [FBLogger log:@"Could not find XCUIPixelsPerSecondForDragGestureVelocity. Using built-in drag velocity presets instead"];
54
+ }
55
+ });
56
+ if (NULL != xctestMapping) {
57
+ return (CGFloat)xctestMapping(velocity);
58
+ }
59
+ if (isSlow) {
60
+ return 250;
61
+ }
62
+ if (isFast) {
63
+ return 750;
64
+ }
65
+ return isDefault ? 500 : velocity;
66
+ }
67
+
68
+ static BOOL FBIsFinitePoint(CGPoint point)
69
+ {
70
+ return isfinite(point.x) && isfinite(point.y);
71
+ }
72
+
23
73
  @implementation XCUIApplication (FBTouchAction)
24
74
 
25
75
  + (BOOL)handleEventSynthesWithError:(NSError *)error
@@ -65,6 +115,80 @@
65
115
  return YES;
66
116
  }
67
117
 
118
+ - (BOOL)fb_pressAtCoordinate:(XCUICoordinate *)startCoordinate
119
+ forDuration:(NSTimeInterval)pressDuration
120
+ thenDragToCoordinate:(XCUICoordinate *)endCoordinate
121
+ withVelocity:(XCUIGestureVelocity)velocity
122
+ thenHoldForDuration:(NSTimeInterval)holdDuration
123
+ error:(NSError **)error
124
+ {
125
+ CGFloat pointsPerSecond = FBPointsPerSecondForDragVelocity(velocity);
126
+ if (!isfinite(pressDuration) || pressDuration < 0 || !isfinite(holdDuration) || holdDuration < 0) {
127
+ NSString *reason = [NSString stringWithFormat:@"Drag press and hold durations must be non-negative numbers of seconds. Got %@ and %@", @(pressDuration), @(holdDuration)];
128
+ @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:nil];
129
+ }
130
+
131
+ // Resolved once, here, so the points validated below are exactly the ones sent. XCTest's own
132
+ // drag re-resolves them inside its event dispatch instead, after re-snapshotting the application.
133
+ CGPoint startPoint = startCoordinate.screenPoint;
134
+ CGPoint endPoint = endCoordinate.screenPoint;
135
+ if (!FBIsFinitePoint(startPoint) || !FBIsFinitePoint(endPoint)) {
136
+ return [[[FBErrorBuilder builder]
137
+ withDescriptionFormat:@"Cannot resolve the on-screen position of the drag gesture (from %@ to %@). "
138
+ "This usually means '%@' did not deliver a valid accessibility snapshot, e.g. because it is busy "
139
+ "or still transitioning after an application switch. Make sure it is idle and retry",
140
+ NSStringFromCGPoint(startPoint), NSStringFromCGPoint(endPoint), self.bundleID]
141
+ buildError:error];
142
+ }
143
+
144
+ // Like XCTest, target the display of the element the start coordinate belongs to. displayID
145
+ // only reads the element's last snapshot, which resolving screenPoint above has just refreshed.
146
+ XCUIElement *referencedElement = startCoordinate.referencedElement;
147
+ long long displayID = [referencedElement respondsToSelector:@selector(displayID)]
148
+ ? referencedElement.displayID
149
+ : 0;
150
+ XCSynthesizedEventRecord *event = [self.class fb_pressDragEventFromPoint:startPoint
151
+ forDuration:pressDuration
152
+ toPoint:endPoint
153
+ pointsPerSecond:pointsPerSecond
154
+ thenHoldForDuration:holdDuration
155
+ displayID:displayID
156
+ interfaceOrientation:self.interfaceOrientation];
157
+ if (![self fb_synthesizeEvent:event error:error]) {
158
+ return NO;
159
+ }
160
+ [self fb_waitUntilStableWithTimeout:FBConfiguration.sharedInstance.animationCoolOffTimeout];
161
+ return YES;
162
+ }
163
+
164
+ + (XCSynthesizedEventRecord *)fb_pressDragEventFromPoint:(CGPoint)startPoint
165
+ forDuration:(NSTimeInterval)pressDuration
166
+ toPoint:(CGPoint)endPoint
167
+ pointsPerSecond:(CGFloat)pointsPerSecond
168
+ thenHoldForDuration:(NSTimeInterval)holdDuration
169
+ displayID:(long long)displayID
170
+ interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
171
+ {
172
+ // Mirrors XCTest's own press-hold-drag event builder
173
+ XCPointerEventPath *path = [[XCPointerEventPath alloc] initForTouchAtPoint:startPoint offset:0];
174
+ if (pressDuration > 0) {
175
+ [path moveToPoint:startPoint atOffset:pressDuration];
176
+ }
177
+ CGFloat distance = (CGFloat)hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
178
+ NSTimeInterval dragEndOffset = pressDuration + distance / pointsPerSecond;
179
+ [path moveToPoint:endPoint atOffset:dragEndOffset];
180
+ [path liftUpAtOffset:dragEndOffset + holdDuration];
181
+
182
+ NSString *name = @"Press, drag and hold";
183
+ XCSynthesizedEventRecord *event = [XCSynthesizedEventRecord instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)]
184
+ ? [[XCSynthesizedEventRecord alloc] initWithName:name
185
+ displayID:(unsigned long long)displayID
186
+ interfaceOrientation:interfaceOrientation]
187
+ : [[XCSynthesizedEventRecord alloc] initWithName:name interfaceOrientation:interfaceOrientation];
188
+ [event addPointerEventPath:path];
189
+ return event;
190
+ }
191
+
68
192
  - (BOOL)fb_synthesizeEvent:(XCSynthesizedEventRecord *)event error:(NSError *__autoreleasing*)error
69
193
  {
70
194
  return [FBXCTestDaemonsProxy synthesizeEventWithRecord:event error:error];
@@ -22,6 +22,7 @@
22
22
  #import "FBRuntimeUtils.h"
23
23
  #import "NSPredicate+FBFormat.h"
24
24
  #import "XCTestPrivateSymbols.h"
25
+ #import "XCUIApplication+FBTouchAction.h"
25
26
  #import "XCUICoordinate.h"
26
27
  #import "XCUIDevice.h"
27
28
  #import "XCUIElement+FBIsVisible.h"
@@ -370,10 +371,14 @@
370
371
  if (nil == endCoordinate) {
371
372
  return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
372
373
  }
373
- [startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
374
- thenDragToCoordinate:endCoordinate
375
- withVelocity:[request.arguments[@"velocity"] doubleValue]
376
- thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
374
+ if (![application fb_pressAtCoordinate:startCoordinate
375
+ forDuration:[request.arguments[@"pressDuration"] doubleValue]
376
+ thenDragToCoordinate:endCoordinate
377
+ withVelocity:[request.arguments[@"velocity"] doubleValue]
378
+ thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]
379
+ error:&error]) {
380
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
381
+ }
377
382
  return FBResponseWithOK();
378
383
  }
379
384
 
@@ -456,7 +461,17 @@
456
461
  return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
457
462
  }
458
463
  NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
459
- [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
464
+ XCUIApplication *application = [target isKindOfClass:XCUIApplication.class]
465
+ ? (XCUIApplication *)target
466
+ : target.application;
467
+ if (![application fb_pressAtCoordinate:startCoordinate
468
+ forDuration:duration
469
+ thenDragToCoordinate:endCoordinate
470
+ withVelocity:XCUIGestureVelocityDefault
471
+ thenHoldForDuration:0
472
+ error:&error]) {
473
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
474
+ }
460
475
  return FBResponseWithOK();
461
476
  }
462
477
 
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.12.11</string>
18
+ <string>16.13.1</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.12.11</string>
22
+ <string>16.13.1</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -340,12 +340,18 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
340
340
 
341
341
  - (void)didClientDisconnect:(nw_connection_t)client
342
342
  {
343
+ BOOL isAwaitingResponse;
343
344
  @synchronized (self.connectionBuffers) {
345
+ isAwaitingResponse = [self.connectionsAwaitingResponse containsObject:client];
344
346
  [self.connectionBuffers removeObjectForKey:client];
345
347
  [self.pendingRequestHeaders removeObjectForKey:client];
346
- [self.connectionsAwaitingResponse removeObject:client];
347
348
  [self.incompleteRequestStarts removeObjectForKey:client];
348
349
  }
350
+ // A client that only stopped sending may still read the response to its in-flight request, so
351
+ // that response's send completion closes the connection instead (it finds no buffer left).
352
+ if (!isAwaitingResponse) {
353
+ nw_connection_cancel(client);
354
+ }
349
355
  }
350
356
 
351
357
  - (void)client:(nw_connection_t)client didReceiveData:(NSData *)data
@@ -881,14 +887,21 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
881
887
  if (nil == queuedSelf) {
882
888
  return;
883
889
  }
890
+ BOOL isDisconnected;
884
891
  @synchronized (queuedSelf.connectionBuffers) {
885
892
  [queuedSelf.connectionsAwaitingResponse removeObject:client];
893
+ isDisconnected = nil == [queuedSelf.connectionBuffers objectForKey:client];
886
894
  // Mid-request connections get their window from when parsing could resume, not from the
887
895
  // previous request. Absent entries stay absent, so idle keep-alives remain exempt.
888
896
  if (nil != [queuedSelf.incompleteRequestStarts objectForKey:client]) {
889
897
  [queuedSelf.incompleteRequestStarts setObject:[NSDate date] forKey:client];
890
898
  }
891
899
  }
900
+ if (isDisconnected) {
901
+ // The peer stopped sending while this request was in flight - see -didClientDisconnect:.
902
+ [queuedSelf closeClient:client];
903
+ return;
904
+ }
892
905
  [queuedSelf processBufferForClient:client];
893
906
  });
894
907
  }];
@@ -36,7 +36,11 @@ NS_ASSUME_NONNULL_BEGIN
36
36
  - (void)client:(nw_connection_t)client didReceiveData:(NSData *)data;
37
37
 
38
38
  /**
39
- The callback which is fired when TCP client disconnects
39
+ The callback which is fired when TCP client disconnects or finishes sending
40
+
41
+ A connection that failed is cancelled by the socket. One whose peer only finished sending stays
42
+ writable (the peer may be awaiting a response), and the delegate must cancel it once it has
43
+ nothing left to send - an uncancelled connection keeps its socket open.
40
44
 
41
45
  @param client The actual disconnected client
42
46
  */
@@ -137,6 +137,8 @@
137
137
  } else if (nw_connection_state_failed == state || nw_connection_state_cancelled == state) {
138
138
  __strong typeof(weakSelf) strongSelf = weakSelf;
139
139
  [strongSelf handleDisconnectForConnection:connection];
140
+ // A failed connection keeps its socket until cancelled (a no-op once cancelled).
141
+ nw_connection_cancel(connection);
140
142
  }
141
143
  });
142
144
  nw_connection_start(connection);
@@ -167,7 +169,14 @@
167
169
  }
168
170
  }
169
171
  }
170
- if (nil != receiveError || (isComplete && nil == content)) {
172
+ if (nil != receiveError) {
173
+ [strongSelf handleDisconnectForConnection:connection];
174
+ nw_connection_cancel(connection);
175
+ return;
176
+ }
177
+ if (isComplete && nil == content) {
178
+ // The peer is done sending but may still be reading - a half-closed client awaits its
179
+ // response - so the delegate cancels the connection once it has nothing left to send.
171
180
  [strongSelf handleDisconnectForConnection:connection];
172
181
  return;
173
182
  }
@@ -368,6 +368,12 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
368
368
  */
369
369
  @property (atomic, assign) BOOL enforceCustomSnapshots;
370
370
 
371
+ /**
372
+ * The identifier of the display targeted by screenshots.
373
+ * nil means the main display. Reset to nil for each new session.
374
+ */
375
+ @property (atomic, copy, nullable) NSNumber *currentDisplayId;
376
+
371
377
  @end
372
378
 
373
379
  NS_ASSUME_NONNULL_END
@@ -357,6 +357,7 @@ static NSString *const axSettingsClassName = @"AXSettings";
357
357
  FBSetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey, @INT_MAX);
358
358
  self.useClearTextShortcut = YES;
359
359
  self.limitXpathContextScope = YES;
360
+ self.currentDisplayId = nil;
360
361
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
361
362
  self.screenshotOrientationStorage = UIInterfaceOrientationUnknown;
362
363
  #endif
@@ -144,7 +144,9 @@ const CGFloat FBMaxCompressionQuality = 1.0f;
144
144
  }
145
145
 
146
146
  UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
147
- format.scale = scalingFactor;
147
+ // scaledSize already applies the requested factor in points. Preserve the
148
+ // image's pixels-per-point scale instead of applying that factor twice.
149
+ format.scale = image.scale;
148
150
  UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize
149
151
  format:format];
150
152
  UIImageOrientation desiredOrientation = orientation == nil
@@ -221,6 +221,8 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
221
221
  [self.listeningClients removeObject:client];
222
222
  [self.pendingFrameCounts removeObjectForKey:client];
223
223
  }
224
+ // Nothing is owed to a client that stopped listening, so its socket can go right away.
225
+ nw_connection_cancel(client);
224
226
  [FBLogger log:@"Disconnected a client from screenshots broadcast"];
225
227
  }
226
228
 
@@ -17,6 +17,23 @@ NS_ASSUME_NONNULL_BEGIN
17
17
  */
18
18
  + (nullable NSArray<NSDictionary<NSString *, id> *> *)screensWithError:(NSError **)error;
19
19
 
20
+ /**
21
+ The display with the given identifier
22
+
23
+ @param displayID The display identifier, as returned by screensWithError:
24
+ @param error Set if the display is not available
25
+ @return The matching display or nil
26
+ */
27
+ + (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID error:(NSError **)error;
28
+
29
+ /**
30
+ The display selected by the currentDisplayId setting, or the main display if the setting is not set
31
+
32
+ @param error Set if the selected display is no longer available
33
+ @return The selected display or nil
34
+ */
35
+ + (nullable XCUIScreen *)currentScreenWithError:(NSError **)error;
36
+
20
37
  /**
21
38
  The identifier of the main device's display
22
39
  */
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  #import "FBScreen.h"
10
+ #import "FBConfiguration.h"
11
+ #import "FBErrorBuilder.h"
10
12
  #import "XCUIElement+FBIsVisible.h"
11
13
  #import "FBXCodeCompatibility.h"
12
14
  #import "XCUIDevice.h"
@@ -40,6 +42,36 @@
40
42
  return result.copy;
41
43
  }
42
44
 
45
+ + (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID error:(NSError **)error
46
+ {
47
+ NSArray<XCUIScreen *> *screens = [XCUIDevice.sharedDevice screensOrError:error];
48
+ if (nil == screens) {
49
+ return nil;
50
+ }
51
+ for (XCUIScreen *screen in screens) {
52
+ if (screen.displayID == displayID) {
53
+ return screen;
54
+ }
55
+ }
56
+ NSMutableArray<NSNumber *> *availableIDs = [NSMutableArray arrayWithCapacity:screens.count];
57
+ for (XCUIScreen *screen in screens) {
58
+ [availableIDs addObject:@(screen.displayID)];
59
+ }
60
+ [[FBErrorBuilder.builder withDescriptionFormat:@"No display with id %lld is available. Available display ids: [%@]",
61
+ displayID, [availableIDs componentsJoinedByString:@", "]]
62
+ buildError:error];
63
+ return nil;
64
+ }
65
+
66
+ + (nullable XCUIScreen *)currentScreenWithError:(NSError **)error
67
+ {
68
+ NSNumber *displayID = FBConfiguration.sharedInstance.currentDisplayId;
69
+ if (nil == displayID) {
70
+ return XCUIScreen.mainScreen;
71
+ }
72
+ return [self screenWithDisplayID:displayID.longLongValue error:error];
73
+ }
74
+
43
75
  + (long long)displayID
44
76
  {
45
77
  return XCUIScreen.mainScreen.displayID;
@@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
14
14
  @interface FBScreenshot : NSObject
15
15
 
16
16
  /**
17
- Retrieves non-scaled screenshot of the whole screen
17
+ Retrieves non-scaled screenshot of the display selected by the currentDisplayId setting
18
18
 
19
19
  @param quality The number in range 0-3, where 0 is PNG (lossless), 3 is HEIC (lossless), 1- low quality JPEG and 2 - high quality JPEG
20
20
  @param error If there is an error, upon return contains an NSError object that describes the problem.
@@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
24
24
  error:(NSError **)error;
25
25
 
26
26
  /**
27
- Retrieves non-scaled screenshot of the whole screen
27
+ Retrieves non-scaled screenshot of the display selected by the currentDisplayId setting
28
28
 
29
29
  @param screenID The screen identifier to take the screenshot from
30
30
  @param compressionQuality Normalized screenshot quality value in range 0..1, where 1 is the best quality
@@ -15,6 +15,7 @@
15
15
  #import "FBImageProcessor.h"
16
16
  #import "FBLogger.h"
17
17
  #import "FBMacros.h"
18
+ #import "FBScreen.h"
18
19
  #import "FBXCodeCompatibility.h"
19
20
  #import "FBXCTestDaemonsProxy.h"
20
21
  #import "XCTMessagingChannel_RunnerToDaemon-Protocol.h"
@@ -60,8 +61,11 @@ NSString *formatTimeInterval(NSTimeInterval interval) {
60
61
  + (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality
61
62
  error:(NSError **)error
62
63
  {
63
- XCUIScreen *mainScreen = XCUIScreen.mainScreen;
64
- return [self.class takeWithScreenID:mainScreen.displayID
64
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
65
+ if (nil == screen) {
66
+ return nil;
67
+ }
68
+ return [self.class takeWithScreenID:screen.displayID
65
69
  scale:SCREENSHOT_SCALE
66
70
  compressionQuality:[self.class compressionQualityWithQuality:quality]
67
71
  sourceUTI:[self.class imageUtiWithQuality:quality]
@@ -46,5 +46,6 @@ extern NSString *const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_S
46
46
  extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE;
47
47
  extern NSString *const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE;
48
48
  extern NSString *const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS;
49
+ extern NSString *const FB_SETTING_CURRENT_DISPLAY_ID;
49
50
 
50
51
  NS_ASSUME_NONNULL_END
@@ -42,3 +42,4 @@ NSString* const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE =
42
42
  NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource";
43
43
  NSString* const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE = @"includeCustomActionsInPageSource";
44
44
  NSString* const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS = @"enforceCustomSnapshots";
45
+ NSString* const FB_SETTING_CURRENT_DISPLAY_ID = @"currentDisplayId";
@@ -12,6 +12,7 @@
12
12
  #import "FBClassChainQueryParser.h"
13
13
  #import "FBCommandStatus.h"
14
14
  #import "FBConfiguration.h"
15
+ #import "FBScreen.h"
15
16
  #import "FBSession.h"
16
17
  #import "FBSettings.h"
17
18
 
@@ -33,6 +34,7 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
33
34
  FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR,
34
35
  FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR,
35
36
  FB_SETTING_AUTO_CLICK_ALERT_SELECTOR,
37
+ FB_SETTING_CURRENT_DISPLAY_ID,
36
38
  nil];
37
39
  });
38
40
  return keys;
@@ -190,6 +192,25 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
190
192
  FBConfiguration.sharedInstance.limitXpathContextScope = [value boolValue];
191
193
  return nil;
192
194
  };
195
+ map[FB_SETTING_CURRENT_DISPLAY_ID] = ^FBCommandStatus *(FBSession *session, id value) {
196
+ if (nil == value) {
197
+ FBConfiguration.sharedInstance.currentDisplayId = nil;
198
+ return nil;
199
+ }
200
+ if (![value isKindOfClass:NSNumber.class]
201
+ || CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()
202
+ || [value compare:@([value longLongValue])] != NSOrderedSame) {
203
+ return [FBCommandStatus invalidArgumentErrorWithMessage:@"currentDisplayId must be an integer display id returned by /wda/screens"
204
+ traceback:nil];
205
+ }
206
+ NSError *error;
207
+ if (nil == [FBScreen screenWithDisplayID:[value longLongValue] error:&error]) {
208
+ return [FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
209
+ traceback:nil];
210
+ }
211
+ FBConfiguration.sharedInstance.currentDisplayId = @([value longLongValue]);
212
+ return nil;
213
+ };
193
214
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
194
215
  map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^FBCommandStatus *(FBSession *session, id value) {
195
216
  NSError *error;
@@ -310,6 +331,9 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
310
331
  map[FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] = ^id(FBSession *session) {
311
332
  return @(FBConfiguration.sharedInstance.limitXpathContextScope);
312
333
  };
334
+ map[FB_SETTING_CURRENT_DISPLAY_ID] = ^id(FBSession *session) {
335
+ return FBConfiguration.sharedInstance.currentDisplayId ?: @([FBScreen displayID]);
336
+ };
313
337
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
314
338
  map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^id(FBSession *session) {
315
339
  return FBConfiguration.sharedInstance.humanReadableScreenshotOrientation;