appium-webdriveragent 16.14.0 → 16.14.2

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.
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <XCTest/XCTest.h>
10
+
11
+ #import "FBIntegrationTestCase.h"
12
+ #import "FBConfiguration.h"
13
+ #import "FBScreen.h"
14
+ #import "FBRunLoopSpinner.h"
15
+ #import "FBExceptions.h"
16
+ #import "FBElementCommands.h"
17
+ #import "FBElementCache.h"
18
+ #import "XCUIElement+FBWebDriverAttributes.h"
19
+
20
+ @interface FBElementCommands (CoordinateTests)
21
+ + (nullable XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset element:(XCUIElement *)element error:(NSError **)error;
22
+ @end
23
+
24
+ #import "XCUIApplication+FBTouchAction.h"
25
+ #import "XCUIElement+FBScrolling.h"
26
+ #import "XCUIDevice+FBRotation.h"
27
+ #import "XCUIDevice+FBHinge.h"
28
+
29
+ @interface FBDisplayGestureTests : FBIntegrationTestCase
30
+ @property (nonatomic) NSNumber *previousDisplayId;
31
+ @property (nonatomic) XCUIElement *canvas;
32
+ @property (nonatomic) FBElementCache *actionElementCache;
33
+ @end
34
+
35
+ @implementation FBDisplayGestureTests
36
+
37
+ - (void)setUp
38
+ {
39
+ [super setUp];
40
+ self.previousDisplayId = FBConfiguration.sharedInstance.currentDisplayId;
41
+ self.actionElementCache = nil;
42
+ if (!XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection) {
43
+ [self resetOrientation];
44
+ }
45
+ [self launchApplication];
46
+ [self.testedApplication.buttons[@"coordinate-probe"] tap];
47
+ self.canvas = self.testedApplication.otherElements[@"coordinate-canvas"];
48
+
49
+ [self selectFixtureDisplay];
50
+ }
51
+
52
+ - (void)selectFixtureDisplay
53
+ {
54
+ // The fixture reports the screen it actually occupies. Do not assume the main
55
+ // screen is active: Duo changes displays when its hinge changes position.
56
+ NSArray<NSNumber *> *size = [self measurement][@"screenSize"];
57
+ NSNumber *displayId = nil;
58
+ for (NSDictionary *screen in [FBScreen screensWithError:nil]) {
59
+ CGFloat width = [screen[@"bounds"][@"width"] doubleValue] / [screen[@"scale"] doubleValue];
60
+ CGFloat height = [screen[@"bounds"][@"height"] doubleValue] / [screen[@"scale"] doubleValue];
61
+ if (fabs(MAX(width, height) - MAX(size[0].doubleValue, size[1].doubleValue)) < 1
62
+ && fabs(MIN(width, height) - MIN(size[0].doubleValue, size[1].doubleValue)) < 1) {
63
+ displayId = screen[@"displayId"];
64
+ break;
65
+ }
66
+ }
67
+ XCTAssertNotNil(displayId, @"Cannot identify the fixture's display");
68
+ FBConfiguration.sharedInstance.currentDisplayId = displayId;
69
+ }
70
+
71
+ - (void)tearDown
72
+ {
73
+ FBConfiguration.sharedInstance.currentDisplayId = self.previousDisplayId;
74
+ [super tearDown];
75
+ }
76
+
77
+ - (NSDictionary *)measurement
78
+ {
79
+ NSString *value = self.testedApplication.staticTexts[@"probe-status"].label;
80
+ NSError *error = nil;
81
+ NSDictionary *result = [NSJSONSerialization JSONObjectWithData:[value dataUsingEncoding:NSUTF8StringEncoding]
82
+ options:0 error:&error];
83
+ XCTAssertNil(error);
84
+ return result;
85
+ }
86
+
87
+ - (void)performMoves:(NSArray<NSDictionary *> *)moves expectedEnd:(CGPoint)expected
88
+ {
89
+ NSUInteger count = [[self measurement][@"count"] unsignedIntegerValue];
90
+ NSMutableArray *items = [NSMutableArray arrayWithArray:@[
91
+ moves.firstObject,
92
+ @{@"type": @"pointerDown", @"button": @0},
93
+ @{@"type": @"pause", @"duration": @100},
94
+ ]];
95
+ if (moves.count > 1) {
96
+ [items addObjectsFromArray:[moves subarrayWithRange:NSMakeRange(1, moves.count - 1)]];
97
+ }
98
+ [items addObject:@{@"type": @"pointerUp", @"button": @0}];
99
+ NSError *error = nil;
100
+ NSArray *actions = @[@{
101
+ @"type": @"pointer", @"id": @"finger", @"parameters": @{@"pointerType": @"touch"}, @"actions": items,
102
+ }];
103
+ XCTAssertTrue([self.testedApplication fb_performW3CActions:actions elementCache:self.actionElementCache error:&error], @"%@", error);
104
+ NSDictionary *result = [self measurement];
105
+ NSLog(@"W3C_DELIVERED expected=%@ actual=%@ count=%@ phase=%@", NSStringFromCGPoint(expected), result[@"last"], result[@"count"], result[@"phase"]);
106
+ XCTAssertEqual([result[@"count"] unsignedIntegerValue], count + 1);
107
+ XCTAssertEqualObjects(result[@"phase"], @"ended");
108
+ NSArray<NSNumber *> *actual = result[@"last"];
109
+ XCTAssertEqualWithAccuracy(actual[0].doubleValue, expected.x, 1);
110
+ XCTAssertEqualWithAccuracy(actual[1].doubleValue, expected.y, 1);
111
+ }
112
+
113
+ - (NSDictionary *)moveFrom:(id)origin x:(CGFloat)x y:(CGFloat)y duration:(NSUInteger)duration
114
+ {
115
+ if (nil != self.actionElementCache && [origin isKindOfClass:XCUIElement.class]) {
116
+ NSString *uuid = [self.actionElementCache storeElement:origin];
117
+ XCTAssertNotNil(uuid);
118
+ origin = @{@"element-6066-11e4-a52e-4f735466cecf": uuid};
119
+ }
120
+ return @{@"type": @"pointerMove", @"origin": origin, @"x": @(x), @"y": @(y), @"duration": @(duration)};
121
+ }
122
+
123
+ - (void)verifyNativeApplicationCoordinates
124
+ {
125
+ NSDictionary *geometry = [self measurement];
126
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
127
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
128
+ for (NSNumber *x in @[@40, @(bounds[0].doubleValue - 40)]) {
129
+ for (NSNumber *y in @[@40, @(bounds[1].doubleValue - 40)]) {
130
+ NSUInteger count = [[self measurement][@"count"] unsignedIntegerValue];
131
+ NSError *error = nil;
132
+ XCUICoordinate *coordinate = [FBElementCommands gestureCoordinateWithOffset:
133
+ CGVectorMake(frame[0].doubleValue + x.doubleValue, frame[1].doubleValue + y.doubleValue)
134
+ element:self.testedApplication error:&error];
135
+ XCTAssertNotNil(coordinate);
136
+ XCTAssertNil(error);
137
+ [coordinate tap];
138
+ NSDictionary *result = [self measurement];
139
+ XCTAssertEqual([result[@"count"] unsignedIntegerValue], count + 1);
140
+ XCTAssertEqualWithAccuracy([result[@"last"][0] doubleValue], x.doubleValue, 1);
141
+ XCTAssertEqualWithAccuracy([result[@"last"][1] doubleValue], y.doubleValue, 1);
142
+ }
143
+ }
144
+ }
145
+
146
+ - (void)verifyNativeEdgeCoordinates
147
+ {
148
+ NSArray<NSNumber *> *size = [self measurement][@"windowSize"];
149
+ CGFloat width = size[0].doubleValue;
150
+ CGFloat height = size[1].doubleValue;
151
+ NSError *error = nil;
152
+ XCUICoordinate *center = [FBElementCommands gestureCoordinateWithOffset:CGVectorMake(width / 2, height / 2)
153
+ element:self.testedApplication error:&error];
154
+ XCTAssertNotNil(center);
155
+ XCTAssertNil(error);
156
+ for (NSValue *value in @[
157
+ [NSValue valueWithCGPoint:CGPointMake(width, height / 2)],
158
+ [NSValue valueWithCGPoint:CGPointMake(width / 2, height)],
159
+ [NSValue valueWithCGPoint:CGPointMake(width + 30, height / 2)],
160
+ [NSValue valueWithCGPoint:CGPointMake(width / 2, height + 30)],
161
+ [NSValue valueWithCGPoint:CGPointMake(-30, height / 2)],
162
+ [NSValue valueWithCGPoint:CGPointMake(width / 2, -30)],
163
+ ]) {
164
+ CGPoint point = value.CGPointValue;
165
+ error = nil;
166
+ XCUICoordinate *coordinate = [FBElementCommands gestureCoordinateWithOffset:CGVectorMake(point.x, point.y)
167
+ element:self.testedApplication error:&error];
168
+ XCTAssertNotNil(coordinate, @"%@", error);
169
+ XCTAssertNil(error);
170
+ if (nil == coordinate) {
171
+ continue;
172
+ }
173
+ // Check extrapolation, not just acceptance: boundary points must not clamp.
174
+ CGPoint expected = [center coordinateWithOffset:CGVectorMake(point.x - width / 2, point.y - height / 2)].screenPoint;
175
+ XCTAssertEqualWithAccuracy(coordinate.screenPoint.x, expected.x, 1);
176
+ XCTAssertEqualWithAccuracy(coordinate.screenPoint.y, expected.y, 1);
177
+ }
178
+ }
179
+
180
+ - (void)testNativeEdgeCoordinates
181
+ {
182
+ [self verifyNativeEdgeCoordinates];
183
+ }
184
+
185
+ - (void)testNativeApplicationCoordinates
186
+ {
187
+ [self verifyNativeApplicationCoordinates];
188
+ [self verifyNativeEdgeCoordinates];
189
+ }
190
+
191
+ - (void)testNativeMainDisplayDefaultCoordinates
192
+ {
193
+ XCTSkipIf(FBConfiguration.sharedInstance.currentDisplayId.longLongValue != [FBScreen displayID],
194
+ @"Requires the fixture on the main display");
195
+ FBConfiguration.sharedInstance.currentDisplayId = nil;
196
+ [self verifyNativeApplicationCoordinates];
197
+ FBConfiguration.sharedInstance.currentDisplayId = @([FBScreen displayID]);
198
+ [self verifyNativeApplicationCoordinates];
199
+ [self verifyNativeEdgeCoordinates];
200
+ }
201
+
202
+ - (void)verifyNativeCoordinatesInOrientation:(UIDeviceOrientation)orientation
203
+ {
204
+ XCTSkipIf(XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection,
205
+ @"Foldable orientations are covered by testNativeCoordinatesAcrossFoldStates");
206
+ UIDeviceOrientation previousOrientation = XCUIDevice.sharedDevice.orientation;
207
+ [self addTeardownBlock:^{
208
+ [XCUIDevice.sharedDevice fb_setDeviceInterfaceOrientation:previousOrientation];
209
+ }];
210
+ XCTAssertTrue([XCUIDevice.sharedDevice fb_setDeviceInterfaceOrientation:orientation]);
211
+ XCTAssertTrue([[[FBRunLoopSpinner new] timeout:5] spinUntilTrue:^BOOL {
212
+ NSDictionary *geometry = [self measurement];
213
+ NSArray<NSNumber *> *size = geometry[@"windowSize"];
214
+ return size[0].doubleValue > size[1].doubleValue
215
+ && ![geometry[@"rotationInProgress"] boolValue];
216
+ }]);
217
+ [self verifyNativeApplicationCoordinates];
218
+ [self verifyNativeEdgeCoordinates];
219
+ }
220
+
221
+ - (void)testNativeCoordinatesInLandscapeLeft
222
+ {
223
+ [self verifyNativeCoordinatesInOrientation:UIDeviceOrientationLandscapeLeft];
224
+ }
225
+
226
+ - (void)testNativeCoordinatesInLandscapeRight
227
+ {
228
+ [self verifyNativeCoordinatesInOrientation:UIDeviceOrientationLandscapeRight];
229
+ }
230
+
231
+ - (void)testNativeCoordinatesRejectMismatchedWindowDisplay
232
+ {
233
+ NSNumber *fixtureDisplay = FBConfiguration.sharedInstance.currentDisplayId;
234
+ NSArray<NSNumber *> *frame = [self measurement][@"canvasWindowRect"];
235
+ BOOL checkedSecondaryDisplay = NO;
236
+ for (NSDictionary *screen in [FBScreen screensWithError:nil]) {
237
+ if ([screen[@"isMain"] boolValue] || [screen[@"displayId"] isEqual:fixtureDisplay]) {
238
+ continue;
239
+ }
240
+ checkedSecondaryDisplay = YES;
241
+ FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
242
+ NSError *error = nil;
243
+ XCUICoordinate *coordinate = [FBElementCommands gestureCoordinateWithOffset:
244
+ CGVectorMake(frame[0].doubleValue + frame[2].doubleValue / 2,
245
+ frame[1].doubleValue + frame[3].doubleValue / 2)
246
+ element:self.testedApplication error:&error];
247
+ XCTAssertNil(coordinate, @"A window on display %@ must not satisfy a request for display %@",
248
+ fixtureDisplay, screen[@"displayId"]);
249
+ XCTAssertNotNil(error);
250
+ }
251
+ XCTSkipIf(!checkedSecondaryDisplay, @"Requires an available secondary display without the fixture");
252
+ }
253
+
254
+ - (void)testNativeCoordinatesAcrossFoldStates
255
+ {
256
+ XCUIDevice *device = XCUIDevice.sharedDevice;
257
+ XCTSkipIf(!device.fb_canAttemptSimulatedHingeAngleInjection, @"Requires a foldable simulator");
258
+ NSError *error = nil;
259
+ NSNumber *originalAngle = [device fb_getSimulatedHingeAngle:&error];
260
+ XCTAssertNotNil(originalAngle, @"%@", error);
261
+ [self addTeardownBlock:^{
262
+ if (nil != originalAngle) {
263
+ [device fb_setSimulatedHingeAngle:originalAngle.doubleValue error:nil];
264
+ }
265
+ }];
266
+ for (NSNumber *angle in @[@0, @90, @180, @0]) {
267
+ [self.testedApplication terminate];
268
+ XCTAssertTrue([device fb_setSimulatedHingeAngle:angle.doubleValue error:&error], @"%@", error);
269
+ [self launchApplication];
270
+ [self.testedApplication.buttons[@"coordinate-probe"] tap];
271
+ [self selectFixtureDisplay];
272
+ [self verifyNativeApplicationCoordinates];
273
+ [self verifyNativeEdgeCoordinates];
274
+ }
275
+ }
276
+
277
+ - (void)verifyW3CApplicationOrigin
278
+ {
279
+ // Resolve serialized element IDs through the same cache as the HTTP API.
280
+ // Passing raw XCTest objects can hide application-root coordinate errors.
281
+ self.actionElementCache = [FBElementCache new];
282
+ NSDictionary *geometry = [self measurement];
283
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
284
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
285
+ CGPoint local = CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2 - 20);
286
+ CGPoint target = CGPointMake(frame[0].doubleValue + local.x, frame[1].doubleValue + local.y);
287
+ // Use the same rect a client receives, not the window's differently oriented
288
+ // frame. On Duo the app rect may still have transposed dimensions.
289
+ CGRect appRect = self.testedApplication.wdFrame;
290
+ NSDictionary *appMove = [self moveFrom:self.testedApplication
291
+ x:target.x - CGRectGetMidX(appRect)
292
+ y:target.y - CGRectGetMidY(appRect) duration:0];
293
+ [self performMoves:@[[self moveFrom:@"viewport" x:target.x y:target.y duration:0]] expectedEnd:local];
294
+ [self performMoves:@[[self moveFrom:self.canvas x:local.x - bounds[0].doubleValue / 2
295
+ y:local.y - bounds[1].doubleValue / 2 duration:0]] expectedEnd:local];
296
+ NSLog(@"W3C_APPLICATION_ORIGIN display=%@ app=%@ window=%@ target=%@", FBConfiguration.sharedInstance.currentDisplayId,
297
+ NSStringFromCGRect(appRect), NSStringFromCGRect(self.testedApplication.windows.firstMatch.frame), NSStringFromCGPoint(target));
298
+ [self performMoves:@[appMove] expectedEnd:local];
299
+ [self performMoves:@[appMove, [self moveFrom:@"pointer" x:0 y:-20 duration:200]]
300
+ expectedEnd:CGPointMake(local.x, local.y - 20)];
301
+ NSMutableDictionary *appDrag = appMove.mutableCopy;
302
+ appDrag[@"duration"] = @200;
303
+ [self performMoves:@[[self moveFrom:self.canvas x:0 y:0 duration:0], appDrag,
304
+ [self moveFrom:@"pointer" x:0 y:-20 duration:200]]
305
+ expectedEnd:CGPointMake(local.x, local.y - 20)];
306
+ [self performMoves:@[appMove, [self moveFrom:self.canvas x:0 y:-40 duration:200],
307
+ [self moveFrom:@"pointer" x:0 y:-20 duration:200]]
308
+ expectedEnd:CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2 - 60)];
309
+ }
310
+
311
+ - (void)testW3CApplicationOrigin
312
+ {
313
+ [self verifyW3CApplicationOrigin];
314
+ }
315
+
316
+ - (void)testW3CApplicationOriginAcrossOrientations
317
+ {
318
+ XCTSkipIf(XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection, @"Covered by fold states");
319
+ [self addTeardownBlock:^{ [self resetOrientation]; }];
320
+ for (NSNumber *orientation in @[@(UIDeviceOrientationLandscapeLeft), @(UIDeviceOrientationLandscapeRight)]) {
321
+ [self resetOrientation];
322
+ XCTAssertTrue([[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation.integerValue]);
323
+ [self verifyW3CApplicationOrigin];
324
+ }
325
+ }
326
+
327
+ - (void)testW3CApplicationOriginAcrossFoldStates
328
+ {
329
+ XCUIDevice *device = XCUIDevice.sharedDevice;
330
+ XCTSkipIf(!device.fb_canAttemptSimulatedHingeAngleInjection, @"Requires a foldable simulator");
331
+ NSError *error = nil;
332
+ NSNumber *originalAngle = [device fb_getSimulatedHingeAngle:&error];
333
+ XCTAssertNotNil(originalAngle);
334
+ [self addTeardownBlock:^{ if (nil != originalAngle) { [device fb_setSimulatedHingeAngle:originalAngle.doubleValue error:nil]; } }];
335
+ for (NSNumber *angle in @[@0, @180, @90, @0]) {
336
+ [self.testedApplication terminate];
337
+ XCTAssertTrue([device fb_setSimulatedHingeAngle:angle.doubleValue error:&error], @"%@", error);
338
+ XCTAssertTrue([[[FBRunLoopSpinner new] timeout:5] spinUntilTrue:^BOOL {
339
+ NSNumber *reading = [device fb_getSimulatedHingeAngle:nil];
340
+ return nil != reading && fabs(reading.doubleValue - angle.doubleValue) < 1;
341
+ }]);
342
+ [self launchApplication];
343
+ [self.testedApplication.buttons[@"coordinate-probe"] tap];
344
+ self.canvas = self.testedApplication.otherElements[@"coordinate-canvas"];
345
+ [self selectFixtureDisplay];
346
+ [self verifyW3CApplicationOrigin];
347
+ }
348
+ }
349
+
350
+ - (void)testViewportAndElementCorners
351
+ {
352
+ NSDictionary *geometry = [self measurement];
353
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
354
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
355
+ CGFloat width = bounds[0].doubleValue, height = bounds[1].doubleValue;
356
+ for (NSNumber *x in @[@40, @(width - 40)]) {
357
+ for (NSNumber *y in @[@40, @(height - 40)]) {
358
+ CGPoint point = CGPointMake(x.doubleValue, y.doubleValue);
359
+ [self performMoves:@[[self moveFrom:@"viewport"
360
+ x:frame[0].doubleValue + point.x y:frame[1].doubleValue + point.y duration:0]]
361
+ expectedEnd:point];
362
+ [self performMoves:@[[self moveFrom:self.canvas x:point.x - width / 2 y:point.y - height / 2 duration:0]]
363
+ expectedEnd:point];
364
+ }
365
+ }
366
+ }
367
+
368
+ - (void)testElementHitpoint
369
+ {
370
+ NSArray<NSNumber *> *bounds = [self measurement][@"canvasBounds"];
371
+ [self performMoves:@[@{@"type": @"pointerMove", @"origin": self.canvas, @"duration": @0}]
372
+ expectedEnd:CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2)];
373
+ }
374
+
375
+ - (void)testPointerRelativeMovesPreserveOriginSpace
376
+ {
377
+ NSDictionary *geometry = [self measurement];
378
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
379
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
380
+ CGPoint center = CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2);
381
+ for (NSDictionary *start in @[
382
+ [self moveFrom:self.canvas x:0 y:0 duration:0],
383
+ [self moveFrom:@"viewport" x:frame[0].doubleValue + center.x y:frame[1].doubleValue + center.y duration:0],
384
+ ]) {
385
+ [self performMoves:@[start,
386
+ [self moveFrom:@"pointer" x:0 y:-40 duration:200],
387
+ [self moveFrom:@"pointer" x:0 y:-40 duration:200]]
388
+ expectedEnd:CGPointMake(center.x, center.y - 80)];
389
+ }
390
+ }
391
+
392
+ - (void)testMixedOriginsWithinOneTouch
393
+ {
394
+ NSDictionary *geometry = [self measurement];
395
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
396
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
397
+ CGPoint center = CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2);
398
+ for (NSNumber *startAtElement in @[@YES, @NO]) {
399
+ NSDictionary *start = startAtElement.boolValue
400
+ ? [self moveFrom:self.canvas x:0 y:0 duration:0]
401
+ : [self moveFrom:@"viewport" x:frame[0].doubleValue + center.x y:frame[1].doubleValue + center.y duration:0];
402
+ NSDictionary *next = startAtElement.boolValue
403
+ ? [self moveFrom:@"viewport" x:frame[0].doubleValue + center.x y:frame[1].doubleValue + center.y - 40 duration:200]
404
+ : [self moveFrom:self.canvas x:0 y:-40 duration:200];
405
+ [self performMoves:@[start, next, [self moveFrom:@"pointer" x:0 y:-40 duration:200]]
406
+ expectedEnd:CGPointMake(center.x, center.y - 80)];
407
+ }
408
+ }
409
+
410
+ - (void)testScrollRemainsInForegroundAndMovesContent
411
+ {
412
+ [self.testedApplication.buttons[@"Scroll"] tap];
413
+ XCUIElement *table = self.testedApplication.tables[@"probe-table"];
414
+ CGFloat before = [[self measurement][@"scrollY"] doubleValue];
415
+ // Exceeds the per-drag limit, exercising multiple drags in one coordinate space.
416
+ [table fb_scrollDownByNormalizedDistance:1.0];
417
+ XCTAssertEqual(self.testedApplication.state, XCUIApplicationStateRunningForeground);
418
+ CGFloat after = [[self measurement][@"scrollY"] doubleValue];
419
+ XCTAssertGreaterThan(after, before + 10);
420
+ [table fb_scrollUpByNormalizedDistance:0.5];
421
+ XCTAssertLessThan([[self measurement][@"scrollY"] doubleValue], after - 10);
422
+ }
423
+
424
+ - (void)testDirectionalScrollRejectsUnavailableDisplay
425
+ {
426
+ [self.testedApplication.buttons[@"Scroll"] tap];
427
+ XCUIElement *table = self.testedApplication.tables[@"probe-table"];
428
+ CGFloat before = [[self measurement][@"scrollY"] doubleValue];
429
+ NSNumber *displayId = FBConfiguration.sharedInstance.currentDisplayId;
430
+ @try {
431
+ FBConfiguration.sharedInstance.currentDisplayId = @(-1);
432
+ XCTAssertThrowsSpecificNamed([table fb_scrollDownByNormalizedDistance:0.5], NSException, FBInvalidArgumentException);
433
+ XCTAssertThrowsSpecificNamed([table fb_scrollUpByNormalizedDistance:0.5], NSException, FBInvalidArgumentException);
434
+ XCTAssertThrowsSpecificNamed([table fb_scrollLeftByNormalizedDistance:0.5], NSException, FBInvalidArgumentException);
435
+ XCTAssertThrowsSpecificNamed([table fb_scrollRightByNormalizedDistance:0.5], NSException, FBInvalidArgumentException);
436
+ } @finally {
437
+ FBConfiguration.sharedInstance.currentDisplayId = displayId;
438
+ }
439
+ XCTAssertEqualWithAccuracy([[self measurement][@"scrollY"] doubleValue], before, 1);
440
+ }
441
+
442
+ - (void)testScrollToElementRejectsUnavailableDisplay
443
+ {
444
+ [self.testedApplication.buttons[@"Scroll"] tap];
445
+ XCUIElement *cell = self.testedApplication.cells[@"probe-row-13"];
446
+ NSNumber *displayId = FBConfiguration.sharedInstance.currentDisplayId;
447
+ @try {
448
+ FBConfiguration.sharedInstance.currentDisplayId = @(-1);
449
+ NSError *error = nil;
450
+ XCTAssertFalse([cell fb_scrollToVisibleWithError:&error]);
451
+ XCTAssertNotNil(error);
452
+ } @finally {
453
+ FBConfiguration.sharedInstance.currentDisplayId = displayId;
454
+ }
455
+ }
456
+
457
+ - (void)testScrollToElementKeepsTheWholeCellVisible
458
+ {
459
+ [self.testedApplication.buttons[@"Scroll"] tap];
460
+ XCUIElement *cell = self.testedApplication.cells[@"probe-row-13"];
461
+ NSError *error = nil;
462
+ XCTAssertTrue([cell fb_scrollToVisibleWithError:&error], @"%@", error);
463
+ XCTAssertEqual(self.testedApplication.state, XCUIApplicationStateRunningForeground);
464
+ NSArray<NSNumber *> *frame = [self measurement][@"canvasWindowRect"];
465
+ XCTAssertTrue(cell.hittable);
466
+ XCTAssertGreaterThanOrEqual(CGRectGetMinY(cell.frame), frame[1].doubleValue - 1);
467
+ XCTAssertLessThanOrEqual(CGRectGetMaxY(cell.frame), frame[1].doubleValue + frame[3].doubleValue + 1);
468
+ }
469
+
470
+ - (void)verifyLandscapeScrolling:(UIDeviceOrientation)orientation
471
+ {
472
+ XCTSkipIf(XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection,
473
+ @"Foldable simulator orientations are covered by testGesturesAcrossFoldStates");
474
+ UIDeviceOrientation previousOrientation = XCUIDevice.sharedDevice.orientation;
475
+ [self addTeardownBlock:^{
476
+ [XCUIDevice.sharedDevice fb_setDeviceInterfaceOrientation:previousOrientation];
477
+ }];
478
+ XCTAssertTrue([[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]);
479
+ [self.testedApplication.buttons[@"Scroll"] tap];
480
+ XCUIElement *table = self.testedApplication.tables[@"probe-table"];
481
+ // Choose a nearby offscreen row: the phone's landscape table is intentionally
482
+ // short, and this test checks coordinates rather than the 25-scroll limit.
483
+ NSUInteger row = (NSUInteger)ceil(table.frame.size.height / 44) + 2;
484
+ XCUIElement *cell = self.testedApplication.cells[[NSString stringWithFormat:@"probe-row-%lu", (unsigned long)row]];
485
+ NSError *error = nil;
486
+ XCTAssertTrue([cell fb_scrollToVisibleWithError:&error], @"%@", error);
487
+ XCTAssertEqual(self.testedApplication.state, XCUIApplicationStateRunningForeground);
488
+ XCTAssertGreaterThan([[self measurement][@"scrollY"] doubleValue], 10);
489
+ XCTAssertTrue([[self measurement][@"fullyVisibleRows"] containsObject:@(row)]);
490
+ XCTAssertTrue(cell.hittable);
491
+ XCTAssertGreaterThanOrEqual(CGRectGetMinY(cell.frame), CGRectGetMinY(table.frame) - 1);
492
+ XCTAssertLessThanOrEqual(CGRectGetMaxY(cell.frame), CGRectGetMaxY(table.frame) + 1);
493
+ }
494
+
495
+ - (void)testScrollToElementInLandscapeLeft
496
+ {
497
+ [self verifyLandscapeScrolling:UIDeviceOrientationLandscapeLeft];
498
+ }
499
+
500
+ - (void)testScrollToElementInLandscapeRight
501
+ {
502
+ [self verifyLandscapeScrolling:UIDeviceOrientationLandscapeRight];
503
+ }
504
+
505
+ - (void)testGesturesAcrossFoldStates
506
+ {
507
+ XCUIDevice *device = XCUIDevice.sharedDevice;
508
+ XCTSkipIf(!device.fb_canAttemptSimulatedHingeAngleInjection, @"Requires a foldable simulator");
509
+ NSError *error = nil;
510
+ NSNumber *originalAngle = [device fb_getSimulatedHingeAngle:&error];
511
+ XCTAssertNotNil(originalAngle, @"%@", error);
512
+ [self addTeardownBlock:^{
513
+ if (nil != originalAngle) {
514
+ [device fb_setSimulatedHingeAngle:originalAngle.doubleValue error:nil];
515
+ }
516
+ }];
517
+ for (NSNumber *angle in @[@0, @90, @180, @0]) {
518
+ [self.testedApplication terminate];
519
+ XCTAssertTrue([device fb_setSimulatedHingeAngle:angle.doubleValue error:&error], @"%@", error);
520
+ [self launchApplication];
521
+ [self.testedApplication.buttons[@"coordinate-probe"] tap];
522
+ self.canvas = self.testedApplication.otherElements[@"coordinate-canvas"];
523
+ [self selectFixtureDisplay];
524
+ NSDictionary *geometry = [self measurement];
525
+ NSArray<NSNumber *> *bounds = geometry[@"canvasBounds"];
526
+ NSArray<NSNumber *> *frame = geometry[@"canvasWindowRect"];
527
+ CGPoint center = CGPointMake(bounds[0].doubleValue / 2, bounds[1].doubleValue / 2);
528
+ [self performMoves:@[[self moveFrom:@"viewport" x:frame[0].doubleValue + center.x
529
+ y:frame[1].doubleValue + center.y duration:0],
530
+ [self moveFrom:self.canvas x:0 y:-20 duration:200],
531
+ [self moveFrom:@"pointer" x:0 y:-20 duration:200]]
532
+ expectedEnd:CGPointMake(center.x, center.y - 40)];
533
+ [self.testedApplication.buttons[@"Scroll"] tap];
534
+ XCUIElement *table = self.testedApplication.tables[@"probe-table"];
535
+ [table fb_scrollDownByNormalizedDistance:1.0];
536
+ XCTAssertGreaterThan([[self measurement][@"scrollY"] doubleValue], 10);
537
+ XCTAssertEqual(self.testedApplication.state, XCUIApplicationStateRunningForeground);
538
+ }
539
+ }
540
+
541
+ @end