appium-webdriveragent 16.14.1 → 16.15.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 +9 -8
- package/WebDriverAgent.xcodeproj/project.pbxproj +4 -0
- package/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +110 -50
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +8 -1
- package/WebDriverAgentLib/Commands/FBElementCommands.m +30 -1
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +2 -1
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBSession.h +22 -0
- package/WebDriverAgentLib/Routing/FBSession.m +25 -3
- package/WebDriverAgentLib/Utilities/FBMathUtils.h +11 -0
- package/WebDriverAgentLib/Utilities/FBMathUtils.m +38 -0
- package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +121 -13
- package/WebDriverAgentTests/IntegrationApp/COORDINATE_PROBE.md +3 -1
- package/WebDriverAgentTests/IntegrationApp/Classes/FBCoordinateProbeViewController.m +18 -0
- package/WebDriverAgentTests/IntegrationTests/FBDisplayGestureTests.m +541 -0
- package/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +13 -7
- package/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +71 -0
- package/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +126 -0
- package/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +45 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#import "FBMacros.h"
|
|
16
16
|
#import "FBMathUtils.h"
|
|
17
17
|
#import "FBProtocolHelpers.h"
|
|
18
|
+
#import "FBScreen.h"
|
|
18
19
|
#import "FBW3CActionsHelpers.h"
|
|
19
20
|
#import "FBXCodeCompatibility.h"
|
|
20
21
|
#import "FBXCTestDaemonsProxy.h"
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
#import "XCSynthesizedEventRecord.h"
|
|
29
30
|
#import "XCPointerEventPath.h"
|
|
30
31
|
#import "XCPointerEvent.h"
|
|
32
|
+
#import "XCUIScreen.h"
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
static NSString *const FB_KEY_TYPE = @"type";
|
|
@@ -66,7 +68,10 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
66
68
|
#if !TARGET_OS_TV && !TARGET_OS_WATCH
|
|
67
69
|
@interface FBW3CGestureItem : FBBaseGestureItem
|
|
68
70
|
|
|
69
|
-
@property (nullable, readonly, nonatomic)
|
|
71
|
+
@property (nullable, readonly, nonatomic) FBW3CGestureItem *previousItem;
|
|
72
|
+
@property (nonatomic) CGVector displayCorrection;
|
|
73
|
+
@property (nonatomic) BOOL usesDisplayCorrection;
|
|
74
|
+
@property (readonly, nonatomic) CGPoint screenPoint;
|
|
70
75
|
|
|
71
76
|
@end
|
|
72
77
|
|
|
@@ -117,7 +122,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
117
122
|
|
|
118
123
|
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
|
119
124
|
application:(XCUIApplication *)application
|
|
120
|
-
previousItem:(nullable
|
|
125
|
+
previousItem:(nullable FBW3CGestureItem *)previousItem
|
|
121
126
|
offset:(double)offset
|
|
122
127
|
error:(NSError **)error
|
|
123
128
|
{
|
|
@@ -127,6 +132,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
127
132
|
self.application = application;
|
|
128
133
|
self.offset = offset;
|
|
129
134
|
_previousItem = previousItem;
|
|
135
|
+
_usesDisplayCorrection = previousItem.usesDisplayCorrection;
|
|
130
136
|
NSNumber *durationObj = FBOptDuration(actionItem, @0, error);
|
|
131
137
|
if (nil == durationObj) {
|
|
132
138
|
return nil;
|
|
@@ -142,6 +148,14 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
142
148
|
return self;
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
- (CGPoint)screenPoint
|
|
152
|
+
{
|
|
153
|
+
CGPoint point = self.atPosition.screenPoint;
|
|
154
|
+
return self.usesDisplayCorrection
|
|
155
|
+
? CGPointMake(point.x + self.displayCorrection.dx, point.y + self.displayCorrection.dy)
|
|
156
|
+
: point;
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
- (nullable XCUICoordinate *)positionWithError:(NSError **)error
|
|
146
160
|
{
|
|
147
161
|
if (nil == self.previousItem || nil == self.previousItem.atPosition) {
|
|
@@ -158,6 +172,27 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
158
172
|
positionOffset:(nullable NSValue *)positionOffset
|
|
159
173
|
error:(NSError **)error
|
|
160
174
|
{
|
|
175
|
+
XCUIScreen *screen = nil;
|
|
176
|
+
if (element.elementType == XCUIElementTypeApplication) {
|
|
177
|
+
screen = [FBScreen currentScreenWithError:error];
|
|
178
|
+
if (nil == screen) {
|
|
179
|
+
return nil;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (nil != screen && screen.displayID != XCUIScreen.mainScreen.displayID) {
|
|
183
|
+
// Unlike ordinary elements, application-root coordinates rotate using the
|
|
184
|
+
// main display. Resolve the client-visible rect's center into viewport
|
|
185
|
+
// coordinates, then let screenPoint apply the selected-display correction.
|
|
186
|
+
// Do not use a normalized app center: XCTest may report a portrait app rect
|
|
187
|
+
// for a landscape window, so its normalized center uses a different space.
|
|
188
|
+
CGRect frame = element.wdFrame;
|
|
189
|
+
if (CGRectIsEmpty(frame)) {
|
|
190
|
+
return [super hitpointWithElement:element positionOffset:positionOffset error:error];
|
|
191
|
+
}
|
|
192
|
+
CGPoint offset = nil == positionOffset ? CGPointZero : positionOffset.CGPointValue;
|
|
193
|
+
CGPoint point = CGPointMake(CGRectGetMidX(frame) + offset.x, CGRectGetMidY(frame) + offset.y);
|
|
194
|
+
return [super hitpointWithElement:nil positionOffset:[NSValue valueWithCGPoint:point] error:error];
|
|
195
|
+
}
|
|
161
196
|
if (nil == element || nil == positionOffset) {
|
|
162
197
|
return [super hitpointWithElement:element positionOffset:positionOffset error:error];
|
|
163
198
|
}
|
|
@@ -223,7 +258,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
223
258
|
}
|
|
224
259
|
}
|
|
225
260
|
if (nil == self.pressure) {
|
|
226
|
-
XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.
|
|
261
|
+
XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.screenPoint
|
|
227
262
|
offset:FBMillisToSeconds(self.offset)];
|
|
228
263
|
return @[result];
|
|
229
264
|
}
|
|
@@ -280,6 +315,10 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
280
315
|
}
|
|
281
316
|
|
|
282
317
|
if (nil != element) {
|
|
318
|
+
// Application origins need viewport display correction; other elements use their own display.
|
|
319
|
+
// This correction does not fix XCTest's legacy-SDK compatibility issue:
|
|
320
|
+
// see docs/multiple-displays.md#legacy-sdk-compatibility-windows.
|
|
321
|
+
self.usesDisplayCorrection = element.elementType == XCUIElementTypeApplication;
|
|
283
322
|
if (nil == x && nil == y) {
|
|
284
323
|
return [self hitpointWithElement:element positionOffset:nil error:error];
|
|
285
324
|
}
|
|
@@ -287,6 +326,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
287
326
|
}
|
|
288
327
|
|
|
289
328
|
if ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT]) {
|
|
329
|
+
self.usesDisplayCorrection = YES;
|
|
290
330
|
return [self hitpointWithElement:nil positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error];
|
|
291
331
|
}
|
|
292
332
|
|
|
@@ -300,6 +340,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
300
340
|
}
|
|
301
341
|
XCUICoordinate *recentPosition = self.previousItem.atPosition;
|
|
302
342
|
CGVector offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGVectorMake(0, 0) : CGVectorMake(x.floatValue, y.floatValue);
|
|
343
|
+
// Pointer-relative moves keep the coordinate space of their preceding position.
|
|
303
344
|
return [recentPosition coordinateWithOffset:offsetRelativeToRecentPosition];
|
|
304
345
|
}
|
|
305
346
|
|
|
@@ -314,10 +355,10 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
314
355
|
error:(NSError **)error
|
|
315
356
|
{
|
|
316
357
|
if (nil == eventPath) {
|
|
317
|
-
return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.
|
|
358
|
+
return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.screenPoint
|
|
318
359
|
offset:FBMillisToSeconds(self.offset + self.duration)]];
|
|
319
360
|
}
|
|
320
|
-
[eventPath moveToPoint:self.
|
|
361
|
+
[eventPath moveToPoint:self.screenPoint
|
|
321
362
|
atOffset:FBMillisToSeconds(self.offset + self.duration)];
|
|
322
363
|
return @[];
|
|
323
364
|
}
|
|
@@ -637,6 +678,10 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
637
678
|
@end
|
|
638
679
|
|
|
639
680
|
|
|
681
|
+
@interface FBW3CActionsSynthesizer ()
|
|
682
|
+
@property (nonatomic) CGVector displayCorrection;
|
|
683
|
+
@end
|
|
684
|
+
|
|
640
685
|
@implementation FBW3CActionsSynthesizer
|
|
641
686
|
|
|
642
687
|
- (NSArray<NSDictionary<NSString *, id> *> *)preprocessedActionItemsWith:(NSArray<NSDictionary<NSString *, id> *> *)actionItems
|
|
@@ -821,6 +866,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
821
866
|
return nil;
|
|
822
867
|
}
|
|
823
868
|
|
|
869
|
+
gestureItem.displayCorrection = self.displayCorrection;
|
|
824
870
|
[chain addItem:gestureItem];
|
|
825
871
|
}
|
|
826
872
|
|
|
@@ -847,13 +893,10 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
847
893
|
return [self eventPathsWithKeyAction:actionDescription forActionId:actionId error:error];
|
|
848
894
|
}
|
|
849
895
|
|
|
850
|
-
- (nullable
|
|
896
|
+
- (nullable NSArray<NSDictionary<NSString *, id> *> *)validatedActionsWithError:(NSError **)error
|
|
851
897
|
{
|
|
852
|
-
XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc]
|
|
853
|
-
initWithName:@"W3C Touch Action"
|
|
854
|
-
interfaceOrientation:self.application.interfaceOrientation];
|
|
855
898
|
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *actionsMapping = [NSMutableDictionary new];
|
|
856
|
-
NSMutableArray<NSString *> *
|
|
899
|
+
NSMutableArray<NSDictionary<NSString *, id> *> *validatedActions = [NSMutableArray new];
|
|
857
900
|
for (NSDictionary<NSString *, id> *action in self.actions) {
|
|
858
901
|
id actionId = [action objectForKey:FB_KEY_ID];
|
|
859
902
|
if (![actionId isKindOfClass:NSString.class] || 0 == [actionId length]) {
|
|
@@ -883,11 +926,76 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
|
|
|
883
926
|
continue;
|
|
884
927
|
}
|
|
885
928
|
|
|
886
|
-
[
|
|
929
|
+
[validatedActions addObject:action];
|
|
887
930
|
[actionsMapping setObject:action forKey:actionId];
|
|
888
931
|
}
|
|
889
|
-
|
|
890
|
-
|
|
932
|
+
return validatedActions.copy;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
- (BOOL)actionsRequireDisplay:(NSArray<NSDictionary<NSString *, id> *> *)actions
|
|
936
|
+
{
|
|
937
|
+
for (NSDictionary<NSString *, id> *action in actions) {
|
|
938
|
+
if (![action[FB_KEY_TYPE] isEqual:FB_ACTION_TYPE_POINTER]) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
for (NSDictionary *item in action[FB_KEY_ACTIONS]) {
|
|
942
|
+
if (![item[FB_ACTION_ITEM_KEY_TYPE] isEqual:FB_ACTION_ITEM_TYPE_PAUSE]) {
|
|
943
|
+
return YES;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return NO;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
- (nullable XCSynthesizedEventRecord *)eventRecordForActions:(NSArray<NSDictionary<NSString *, id> *> *)actions
|
|
951
|
+
error:(NSError **)error
|
|
952
|
+
{
|
|
953
|
+
// Keyboard and pause-only sequences have no display coordinates. A display
|
|
954
|
+
// disappearing must not prevent these input sources from being synthesized.
|
|
955
|
+
XCUIScreen *screen = [self actionsRequireDisplay:actions] ? [FBScreen currentScreenWithError:error] : XCUIScreen.mainScreen;
|
|
956
|
+
if (nil == screen) {
|
|
957
|
+
return nil;
|
|
958
|
+
}
|
|
959
|
+
XCSynthesizedEventRecord *eventRecord;
|
|
960
|
+
self.displayCorrection = CGVectorMake(0, 0);
|
|
961
|
+
if (screen.displayID == XCUIScreen.mainScreen.displayID) {
|
|
962
|
+
eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:@"W3C Touch Action"
|
|
963
|
+
interfaceOrientation:self.application.interfaceOrientation];
|
|
964
|
+
} else {
|
|
965
|
+
if (![XCSynthesizedEventRecord instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)]) {
|
|
966
|
+
if (error) {
|
|
967
|
+
*error = [[FBErrorBuilder.builder
|
|
968
|
+
withDescription:@"Actions on a display other than the main one are not supported by this XCTest version"] build];
|
|
969
|
+
}
|
|
970
|
+
return nil;
|
|
971
|
+
}
|
|
972
|
+
eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:@"W3C Touch Action"
|
|
973
|
+
displayID:(unsigned long long)screen.displayID
|
|
974
|
+
interfaceOrientation:self.application.interfaceOrientation];
|
|
975
|
+
// Viewport coordinates use the main display's size when rotating screenPoint.
|
|
976
|
+
// Element coordinates already use their own display. Apply this correction
|
|
977
|
+
// per gesture item so mixed origins and pointer-relative moves stay aligned.
|
|
978
|
+
XCUIScreen *mainScreen = XCUIScreen.mainScreen;
|
|
979
|
+
self.displayCorrection = FBDisplayCoordinateOffset(
|
|
980
|
+
CGSizeMake(mainScreen.bounds.size.width / mainScreen.scale, mainScreen.bounds.size.height / mainScreen.scale),
|
|
981
|
+
CGSizeMake(screen.bounds.size.width / screen.scale, screen.bounds.size.height / screen.scale),
|
|
982
|
+
eventRecord.interfaceOrientation);
|
|
983
|
+
}
|
|
984
|
+
return eventRecord;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
|
|
988
|
+
{
|
|
989
|
+
NSArray<NSDictionary<NSString *, id> *> *actions = [self validatedActionsWithError:error];
|
|
990
|
+
if (nil == actions) {
|
|
991
|
+
return nil;
|
|
992
|
+
}
|
|
993
|
+
XCSynthesizedEventRecord *eventRecord = [self eventRecordForActions:actions error:error];
|
|
994
|
+
if (nil == eventRecord) {
|
|
995
|
+
return nil;
|
|
996
|
+
}
|
|
997
|
+
for (NSDictionary<NSString *, id> *actionDescription in actions) {
|
|
998
|
+
NSString *actionId = actionDescription[FB_KEY_ID];
|
|
891
999
|
NSArray<XCPointerEventPath *> *eventPaths = [self eventPathsWithActionDescription:actionDescription forActionId:actionId error:error];
|
|
892
1000
|
if (nil == eventPaths) {
|
|
893
1001
|
return nil;
|
|
@@ -21,7 +21,9 @@ shows 80 numbered rows, each 44 points high.
|
|
|
21
21
|
|
|
22
22
|
The `probe-status` label exposes `start`, `last`, `phase`, and `count` after a
|
|
23
23
|
touch. `start` and `last` are **canvas-local app points**. It also exposes
|
|
24
|
-
`canvasBounds`, `canvasWindowRect`, `windowSize`, `screenSize`,
|
|
24
|
+
`canvasBounds`, `canvasWindowRect`, `windowSize`, `screenSize`, `scrollY`, and
|
|
25
|
+
`fullyVisibleRows` (row indices whose full rectangles lie inside the table bounds).
|
|
26
|
+
`rotationInProgress` stays true until the fixture's rotation transition completes.
|
|
25
27
|
Geometry is refreshed when the view lays out, including after rotation or window
|
|
26
28
|
resizing. Touch-related fields are absent before the first touch.
|
|
27
29
|
|
|
@@ -106,6 +106,17 @@
|
|
|
106
106
|
[self.view addSubview:self.table];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
|
110
|
+
{
|
|
111
|
+
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
112
|
+
self.measurement[@"rotationInProgress"] = @YES;
|
|
113
|
+
[self publishMeasurement];
|
|
114
|
+
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
115
|
+
self.measurement[@"rotationInProgress"] = @NO;
|
|
116
|
+
[self publishMeasurement];
|
|
117
|
+
}];
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
- (void)viewDidLayoutSubviews
|
|
110
121
|
{
|
|
111
122
|
[super viewDidLayoutSubviews];
|
|
@@ -139,6 +150,13 @@
|
|
|
139
150
|
self.measurement[@"windowSize"] = @[@(window.bounds.size.width), @(window.bounds.size.height)];
|
|
140
151
|
self.measurement[@"screenSize"] = @[@(window.screen.bounds.size.width), @(window.screen.bounds.size.height)];
|
|
141
152
|
self.measurement[@"scrollY"] = @(self.table.contentOffset.y);
|
|
153
|
+
NSMutableArray<NSNumber *> *fullyVisibleRows = [NSMutableArray new];
|
|
154
|
+
for (NSIndexPath *indexPath in self.table.indexPathsForVisibleRows) {
|
|
155
|
+
if (CGRectContainsRect(self.table.bounds, [self.table rectForRowAtIndexPath:indexPath])) {
|
|
156
|
+
[fullyVisibleRows addObject:@(indexPath.row)];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
self.measurement[@"fullyVisibleRows"] = fullyVisibleRows;
|
|
142
160
|
NSData *data = [NSJSONSerialization dataWithJSONObject:self.measurement
|
|
143
161
|
options:NSJSONWritingSortedKeys
|
|
144
162
|
error:nil];
|