Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)acti
}
self.duration = durationObj.doubleValue;
XCUICoordinate *position = [self positionWithError:error];
if (nil == position) {
// A pause may legally have no position yet (nil position, no error set)
if (nil == position && error && nil != *error) {
return nil;
}
self.atPosition = position;
Expand All @@ -143,7 +144,7 @@ - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)acti

- (nullable XCUICoordinate *)positionWithError:(NSError **)error
{
if (nil == self.previousItem) {
if (nil == self.previousItem || nil == self.previousItem.atPosition) {
NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE];
if (error) {
*error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
Expand Down Expand Up @@ -205,9 +206,19 @@ + (NSString *)actionName
currentItemIndex:(NSUInteger)currentItemIndex
error:(NSError **)error
{
if (nil != eventPath && currentItemIndex == 1) {
if (nil != eventPath && currentItemIndex >= 1) {
FBW3CGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1];
if ([preceedingItem isKindOfClass:FBPointerMoveItem.class]) {
// Only skip creating a new touch if the preceding pointerMove is the one that
// implicitly opened this touch, i.e. nothing but (possibly zero-duration) pauses
// came before it. Pauses never create an event path themselves.
BOOL isPreceedingMoveTheFirstRealItem = YES;
for (NSInteger index = (NSInteger)currentItemIndex - 2; index >= 0; index--) {
if (![[allItems objectAtIndex:index] isKindOfClass:FBPointerPauseItem.class]) {
isPreceedingMoveTheFirstRealItem = NO;
break;
}
}
if ([preceedingItem isKindOfClass:FBPointerMoveItem.class] && isPreceedingMoveTheFirstRealItem) {
return @[];
}
}
Expand Down Expand Up @@ -280,7 +291,7 @@ - (nullable XCUICoordinate *)positionWithError:(NSError **)error
}

// origin == FB_ORIGIN_TYPE_POINTER
if (nil == self.previousItem) {
if (nil == self.previousItem || nil == self.previousItem.atPosition) {
NSString *errorDescription = [NSString stringWithFormat:@"There is no previous item for '%@' action item, however %@ is set to '%@'", self.actionItem, FB_ACTION_ITEM_KEY_ORIGIN, FB_ORIGIN_TYPE_POINTER];
if (error) {
*error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
Expand Down Expand Up @@ -320,6 +331,13 @@ + (NSString *)actionName
return FB_ACTION_ITEM_TYPE_PAUSE;
}

- (nullable XCUICoordinate *)positionWithError:(NSError **)error
{
// A pause has no position of its own; proxy whatever real move preceded
// it, or nil (not a fabricated point) if none has run yet
return self.previousItem.atPosition;
}

- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
allItems:(NSArray *)allItems
currentItemIndex:(NSUInteger)currentItemIndex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,40 @@ - (void)testSymmetricTwoFingersTap
[self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
}

- (void)testTwoFingersTapWithLeadingZeroDurationPause
{
// Selenium clients pad shorter action sequences with a zero-duration pause
// so that all pointers/devices end up with the same number of ticks
XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName];
NSArray<NSDictionary<NSString *, id> *> *gesture =
@[
@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
@{
@"type": @"pointer",
@"id": @"finger2",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pause", @"duration": @0},
@{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
];

[self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
}

@end

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#import "XCUIDevice+FBRotation.h"
#import "FBRunLoopSpinner.h"
#import "FBXCodeCompatibility.h"
#import "FBW3CActionsSynthesizer.h"
#import "XCSynthesizedEventRecord.h"
#import "XCPointerEventPath.h"
#import "XCPointerEvent.h"

@interface FBW3CTouchActionsIntegrationTestsPart1 : FBIntegrationTestCase
@end
Expand Down Expand Up @@ -185,63 +189,79 @@ - (void)testErroneousGestures
},
],

// Chain element where action items start with an incorrect item
// Chain element where pointerMove action item does not contain coordinates
@[@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
@{@"type": @"pointerMove", @"duration": @0},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
],

// Chain element where pointerMove action item does not contain coordinates
// Chain element where pointerMove action item cannot use coordinates of the previous item
@[@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pointerMove", @"duration": @0},
@{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
],

// Chain element where pointerMove action item cannot use coordinates of the previous item
// Chain element where action items contains negative duration
@[@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
@{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pause", @"duration": @-100},
@{@"type": @"pointerUp"},
],
},
],

// Chain element where action items contains negative duration
// Chain element where a leading pause is followed directly by pointerDown,
// with no real pointerMove ever establishing a position
@[@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pointerMove", @"duration": @0, @"x": @1, @"y": @1},
@{@"type": @"pause", @"duration": @0},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @-100},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
],


// Chain element where a leading pause is followed directly by a relative
// pointerMove, with no real preceding position to be relative to
@[@{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pause", @"duration": @0},
@{@"type": @"pointerMove", @"duration": @0, @"origin": @"pointer"},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
],
},
],

// Chain element where action items start with an incorrect one, because the correct one is canceled
@[@{
@"type": @"pointer",
Expand Down Expand Up @@ -299,6 +319,69 @@ - (void)testTap
[self verifyGesture:gesture orientation:UIDeviceOrientationPortrait];
}

- (void)testLeadingZeroDurationPauseDoesNotAddExtraTouch
{
// A leading pause must not defeat the down-after-move dedup logic in
// FBPointerDownItem and make WDA synthesize a second, separate touch-down
// for the same finger. Inspect the actual synthesized XCTest event stream
// (without dispatching it) rather than only checking the gesture's visible
// side effect, since a duplicate touch at the same point may still produce
// the same visible outcome.
XCUIElement *element = self.testedApplication.buttons[FBShowAlertButtonName];
NSDictionary<NSString *, id> *(^sequenceWithLeadingPause)(BOOL) = ^NSDictionary<NSString *, id> *(BOOL withLeadingPause) {
NSMutableArray<NSDictionary<NSString *, id> *> *actions = [NSMutableArray array];
if (withLeadingPause) {
[actions addObject:@{@"type": @"pause", @"duration": @0}];
}
[actions addObjectsFromArray:@[
@{@"type": @"pointerMove", @"duration": @0, @"origin": element, @"x": @0, @"y": @0},
@{@"type": @"pointerDown"},
@{@"type": @"pause", @"duration": @100},
@{@"type": @"pointerUp"},
]];
return @{
@"type": @"pointer",
@"id": @"finger1",
@"parameters": @{@"pointerType": @"touch"},
@"actions": actions.copy,
};
};

NSError *error;
FBW3CActionsSynthesizer *baselineSynthesizer =
[[FBW3CActionsSynthesizer alloc] initWithActions:@[sequenceWithLeadingPause(NO)]
forApplication:self.testedApplication
elementCache:nil
error:&error];
XCTAssertNotNil(baselineSynthesizer);
XCSynthesizedEventRecord *baselineRecord = [baselineSynthesizer synthesizeWithError:&error];
XCTAssertNotNil(baselineRecord, @"%@", error);

FBW3CActionsSynthesizer *pausedSynthesizer =
[[FBW3CActionsSynthesizer alloc] initWithActions:@[sequenceWithLeadingPause(YES)]
forApplication:self.testedApplication
elementCache:nil
error:&error];
XCTAssertNotNil(pausedSynthesizer);
XCSynthesizedEventRecord *pausedRecord = [pausedSynthesizer synthesizeWithError:&error];
XCTAssertNotNil(pausedRecord, @"%@", error);

XCTAssertEqual(baselineRecord.eventPaths.count, (NSUInteger)1);
XCTAssertEqual(pausedRecord.eventPaths.count, baselineRecord.eventPaths.count);

XCPointerEventPath *baselinePath = baselineRecord.eventPaths.firstObject;
XCPointerEventPath *pausedPath = pausedRecord.eventPaths.firstObject;
XCTAssertEqual(pausedPath.pointerEvents.count, baselinePath.pointerEvents.count);
for (NSUInteger i = 0; i < baselinePath.pointerEvents.count; i++) {
XCPointerEvent *baselineEvent = baselinePath.pointerEvents[i];
XCPointerEvent *pausedEvent = pausedPath.pointerEvents[i];
XCTAssertEqual(pausedEvent.eventType, baselineEvent.eventType);
XCTAssertEqualWithAccuracy(pausedEvent.offset, baselineEvent.offset, 0.001);
XCTAssertEqualWithAccuracy(pausedEvent.coordinate.x, baselineEvent.coordinate.x, 0.001);
XCTAssertEqualWithAccuracy(pausedEvent.coordinate.y, baselineEvent.coordinate.y, 0.001);
}
}

- (void)testDoubleTap
{
NSArray<NSDictionary<NSString *, id> *> *gesture =
Expand Down
Loading