Skip to content
Open
8 changes: 8 additions & 0 deletions WebDriverAgentLib/Utilities/FBMathUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold);
#if !TARGET_OS_TV && !TARGET_OS_WATCH
/*! Inverts size if necessary to match current screen orientation */
CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation);

/*!
Offsets XCTest screenPoint coordinates for an explicitly selected display.
XCTest rotates these coordinates using the main screen's dimensions even when
the synthesized event record targets another display. Sizes are in points.
*/
CGVector FBDisplayCoordinateOffset(CGSize mainSize, CGSize displaySize,
UIInterfaceOrientation orientation);
#endif

#if !TARGET_OS_TV
Expand Down
17 changes: 17 additions & 0 deletions WebDriverAgentLib/Utilities/FBMathUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,20 @@ BOOL FBScrollGestureOffsets(CGRect scrollingFrame,
(endPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
return YES;
}

#if !TARGET_OS_TV && !TARGET_OS_WATCH
CGVector FBDisplayCoordinateOffset(CGSize mainSize, CGSize displaySize,
UIInterfaceOrientation orientation)
{
CGVector offset = CGVectorMake(0, 0);
if (orientation == UIInterfaceOrientationLandscapeRight
|| orientation == UIInterfaceOrientationPortraitUpsideDown) {
offset.dx = displaySize.width - mainSize.width;
}
if (orientation == UIInterfaceOrientationLandscapeLeft
|| orientation == UIInterfaceOrientationPortraitUpsideDown) {
offset.dy = displaySize.height - mainSize.height;
}
return offset;
}
#endif
40 changes: 37 additions & 3 deletions WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "FBMacros.h"
#import "FBMathUtils.h"
#import "FBProtocolHelpers.h"
#import "FBScreen.h"
#import "FBW3CActionsHelpers.h"
#import "FBXCodeCompatibility.h"
#import "FBXCTestDaemonsProxy.h"
Expand All @@ -28,6 +29,7 @@
#import "XCSynthesizedEventRecord.h"
#import "XCPointerEventPath.h"
#import "XCPointerEvent.h"
#import "XCUIScreen.h"


static NSString *const FB_KEY_TYPE = @"type";
Expand Down Expand Up @@ -849,9 +851,35 @@ @implementation FBW3CActionsSynthesizer

- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
{
XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc]
initWithName:@"W3C Touch Action"
interfaceOrientation:self.application.interfaceOrientation];
XCUIScreen *screen = [FBScreen currentScreenWithError:error];
if (nil == screen) {
return nil;
}
XCSynthesizedEventRecord *eventRecord;
CGVector displayCorrection = CGVectorMake(0, 0);
if (screen.isMainScreen) {
eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:@"W3C Touch Action"
interfaceOrientation:self.application.interfaceOrientation];
} else {
if (![XCSynthesizedEventRecord instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)]) {
if (error) {
*error = [[FBErrorBuilder.builder
withDescription:@"Actions on a display other than the main one are not supported by this XCTest version"] build];
}
return nil;
}
eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:@"W3C Touch Action"
displayID:(unsigned long long)screen.displayID
interfaceOrientation:self.application.interfaceOrientation];
// XCUICoordinate.screenPoint uses the main display's size when rotating
// coordinates. Selecting an event display does not change that transform;
// compensate for the difference before dispatching to the selected display.
XCUIScreen *mainScreen = XCUIScreen.mainScreen;
displayCorrection = FBDisplayCoordinateOffset(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this correction is needed? shouldn't xctest automatically adjust coordinates?

CGSizeMake(mainScreen.bounds.size.width / mainScreen.scale, mainScreen.bounds.size.height / mainScreen.scale),
CGSizeMake(screen.bounds.size.width / screen.scale, screen.bounds.size.height / screen.scale),
eventRecord.interfaceOrientation);
}
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *actionsMapping = [NSMutableDictionary new];
NSMutableArray<NSString *> *actionIds = [NSMutableArray new];
for (NSDictionary<NSString *, id> *action in self.actions) {
Expand Down Expand Up @@ -893,6 +921,12 @@ - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
return nil;
}
for (XCPointerEventPath *eventPath in eventPaths) {
if (displayCorrection.dx != 0 || displayCorrection.dy != 0) {
for (XCPointerEvent *pointerEvent in eventPath.pointerEvents) {
CGPoint point = pointerEvent.coordinate;
pointerEvent.coordinate = CGPointMake(point.x + displayCorrection.dx, point.y + displayCorrection.dy);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid correcting coordinates already transformed for the secondary display

This adds the main-to-secondary offset to every pointer event, including element-origin actions whose coordinates are already transformed for the selected display. In Xcode 27.1, XCUICoordinate.screenPoint delegates to point:transformedForEventSynthesisFromOrientation:error:, which selects the screen using the originating element snapshot's displayID. When that snapshot belongs to the selected secondary display, this additional offset shifts taps and drags away from the target.

I verified the transform using the installed XCTest framework with mocked display metadata: with main size 402×874 and secondary size 669×951, the secondary snapshot transforms (100, 100) to (569, 100) in landscape-right; this correction then shifts it to (836, 100). This was framework-level verification, not an end-to-end device run.

Please derive the correction from each coordinate's source display and add secondary-display element-origin coverage. The added integration tests currently exercise viewport origins only.

}
}
[eventRecord addPointerEventPath:eventPath];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#import <XCTest/XCTest.h>

#import "FBIntegrationTestCase.h"
#import "FBConfiguration.h"
#import "FBMathUtils.h"
#import "FBScreen.h"

#import "XCUIElement.h"
#import "XCUIDevice.h"
Expand All @@ -21,6 +24,7 @@
#import "XCSynthesizedEventRecord.h"
#import "XCPointerEventPath.h"
#import "XCPointerEvent.h"
#import "XCUIScreen.h"

@interface FBW3CTouchActionsIntegrationTestsPart1 : FBIntegrationTestCase
@end
Expand Down Expand Up @@ -53,11 +57,100 @@ - (void)setUp

- (void)tearDown
{
FBConfiguration.sharedInstance.currentDisplayId = nil;
[self clearAlert];
[self resetOrientation];
[super tearDown];
}

- (nullable XCSynthesizedEventRecord *)tapRecordWithError:(NSError **)error
{
NSArray *gesture = @[@{
@"type": @"pointer", @"id": @"finger", @"parameters": @{@"pointerType": @"touch"},
@"actions": @[
@{@"type": @"pointerMove", @"x": @100, @"y": @100, @"duration": @0},
@{@"type": @"pointerDown", @"button": @0},
@{@"type": @"pointerUp", @"button": @0},
],
}];
FBW3CActionsSynthesizer *synthesizer = [[FBW3CActionsSynthesizer alloc] initWithActions:gesture
forApplication:self.testedApplication
elementCache:nil
error:error];
return [synthesizer synthesizeWithError:error];
}

- (NSArray<XCPointerEvent *> *)pointerEventsOfRecord:(XCSynthesizedEventRecord *)record
{
XCPointerEventPath *eventPath = record.eventPaths.firstObject;
return eventPath.pointerEvents;
}

- (void)testMainDisplaySettingKeepsDefaultCoordinates
{
for (NSNumber *orientation in @[@(UIDeviceOrientationPortrait),
@(UIDeviceOrientationLandscapeLeft),
@(UIDeviceOrientationLandscapeRight)]) {
[[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation.integerValue];
NSError *error = nil;
FBConfiguration.sharedInstance.currentDisplayId = nil;
XCSynthesizedEventRecord *baseline = [self tapRecordWithError:&error];
FBConfiguration.sharedInstance.currentDisplayId = @([FBScreen displayID]);
XCSynthesizedEventRecord *selected = [self tapRecordWithError:&error];
XCTAssertNotNil(baseline);
XCTAssertNotNil(selected);
XCTAssertNil(error);
NSArray<XCPointerEvent *> *baselineEvents = [self pointerEventsOfRecord:baseline];
NSArray<XCPointerEvent *> *selectedEvents = [self pointerEventsOfRecord:selected];
XCTAssertEqual(baselineEvents.count, selectedEvents.count);
for (NSUInteger i = 0; i < baselineEvents.count; i++) {
XCTAssertTrue(CGPointEqualToPoint(baselineEvents[i].coordinate, selectedEvents[i].coordinate));
}
}
}

- (void)testUnavailableDisplaySettingIsRejected
{
long long maxID = 0;
for (NSDictionary<NSString *, id> *screen in [FBScreen screensWithError:nil]) {
maxID = MAX(maxID, [screen[@"displayId"] longLongValue]);
}
FBConfiguration.sharedInstance.currentDisplayId = @(maxID + 1);
NSError *error = nil;
XCTAssertNil([self tapRecordWithError:&error]);
XCTAssertNotNil(error);
}

- (void)testSecondaryDisplaySetting
{
XCUIScreen *secondary = nil;
for (XCUIScreen *screen in [XCUIDevice.sharedDevice screensOrError:nil]) {
if (!screen.isMainScreen) {
secondary = screen;
break;
}
}
if (nil == secondary) {
XCTSkip(@"The device has no secondary display");
}
NSError *error = nil;
XCSynthesizedEventRecord *baseline = [self tapRecordWithError:&error];
FBConfiguration.sharedInstance.currentDisplayId = @(secondary.displayID);
XCSynthesizedEventRecord *selected = [self tapRecordWithError:&error];
XCTAssertNotNil(selected);
XCTAssertNil(error);
XCTAssertEqual(selected.displayID, (unsigned long long)secondary.displayID);
XCUIScreen *main = XCUIScreen.mainScreen;
CGVector offset = FBDisplayCoordinateOffset(
CGSizeMake(main.bounds.size.width / main.scale, main.bounds.size.height / main.scale),
CGSizeMake(secondary.bounds.size.width / secondary.scale, secondary.bounds.size.height / secondary.scale),
selected.interfaceOrientation);
CGPoint expected = [self pointerEventsOfRecord:baseline].firstObject.coordinate;
CGPoint actual = [self pointerEventsOfRecord:selected].firstObject.coordinate;
XCTAssertEqual(actual.x, expected.x + offset.dx);
XCTAssertEqual(actual.y, expected.y + offset.dy);
}

- (void)testErroneousGestures
{
NSArray<NSArray<NSDictionary<NSString *, id> *> *> *invalidGestures =
Expand Down
30 changes: 30 additions & 0 deletions WebDriverAgentTests/UnitTests/FBMathUtilsTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,34 @@ - (void)testScrollGestureOffsetsWithEmptyFrame
XCTAssertTrue(endOffset.dx == -1 && endOffset.dy == -1);
}

- (void)testDisplayCoordinateOffsets
{
CGSize mainSize = CGSizeMake(402, 874);
CGSize innerSize = CGSizeMake(669, 951);
NSArray<NSNumber *> *orientations = @[
@(UIInterfaceOrientationPortrait),
@(UIInterfaceOrientationLandscapeLeft),
@(UIInterfaceOrientationLandscapeRight),
@(UIInterfaceOrientationPortraitUpsideDown),
];
NSArray<NSValue *> *expected = @[
[NSValue valueWithCGPoint:CGPointZero],
[NSValue valueWithCGPoint:CGPointMake(0, 77)],
[NSValue valueWithCGPoint:CGPointMake(267, 0)],
[NSValue valueWithCGPoint:CGPointMake(267, 77)],
];
for (NSUInteger i = 0; i < orientations.count; i++) {
UIInterfaceOrientation orientation = orientations[i].integerValue;
CGVector offset = FBDisplayCoordinateOffset(mainSize, innerSize, orientation);
XCTAssertEqual(offset.dx, expected[i].CGPointValue.x);
XCTAssertEqual(offset.dy, expected[i].CGPointValue.y);
CGVector sameDisplay = FBDisplayCoordinateOffset(mainSize, mainSize, orientation);
XCTAssertEqual(sameDisplay.dx, 0);
XCTAssertEqual(sameDisplay.dy, 0);
CGVector reverse = FBDisplayCoordinateOffset(innerSize, mainSize, orientation);
XCTAssertEqual(reverse.dx, -expected[i].CGPointValue.x);
XCTAssertEqual(reverse.dy, -expected[i].CGPointValue.y);
}
}

@end
Loading