blob: 71f5a88796d76aa3d356f277939853d9316c5693 [file] [log] [blame]
// Copyright 2020 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <XCTest/XCTest.h>
static const NSInteger kSecondsToWaitForFlutterView = 30;
@interface iPadGestureTests : XCTestCase
@end
@implementation iPadGestureTests
- (void)setUp {
[super setUp];
self.continueAfterFailure = NO;
}
static BOOL performBoolSelector(id target, SEL selector) {
NSInvocation* invocation = [NSInvocation
invocationWithMethodSignature:[[target class] instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:target];
[invocation invoke];
BOOL returnValue;
[invocation getReturnValue:&returnValue];
return returnValue;
}
static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, NSString* message) {
NSMutableArray<NSNumber*>* matchingMessages = messages[message];
XCTAssertNotNil(matchingMessages, @"Did not receive \"%@\" message", message);
XCTAssertEqual(matchingMessages.count, 1, @"More than one \"%@\" message", message);
return matchingMessages.firstObject.intValue;
}
// TODO(85810): Remove reflection in this test when Xcode version is upgraded to 13.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
- (void)testPointerButtons {
BOOL supportsPointerInteraction = NO;
SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
supportsPointerInteraction =
performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
}
XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
XCUIApplication* app = [[XCUIApplication alloc] init];
app.launchArguments = @[ @"--pointer-events" ];
[app launch];
NSPredicate* predicateToFindFlutterView =
[NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
elementMatchingPredicate:predicateToFindFlutterView];
if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
NSLog(@"%@", app.debugDescription);
XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
@(kSecondsToWaitForFlutterView));
}
XCTAssertNotNil(flutterView);
[flutterView tap];
// Initial add event should have buttons = 0
XCTAssertTrue(
[app.textFields[@"0,PointerChange.add,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.add event did not occur for a normal tap");
// Normal tap should have buttons = 0, the flutter framework will ensure it has buttons = 1
XCTAssertTrue(
[app.textFields[@"1,PointerChange.down,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.down event did not occur for a normal tap");
XCTAssertTrue(
[app.textFields[@"2,PointerChange.up,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.up event did not occur for a normal tap");
XCTAssertTrue(
[app.textFields[@"3,PointerChange.remove,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.remove event did not occur for a normal tap");
SEL rightClick = @selector(rightClick);
XCTAssertTrue([flutterView respondsToSelector:rightClick],
@"If supportsPointerInteraction is true, this should be true too.");
[flutterView performSelector:rightClick];
// On simulated right click, a hover also occurs, so the hover pointer is added
XCTAssertTrue(
[app.textFields[@"4,PointerChange.add,device=1,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.add event did not occur for a right-click's hover pointer");
// The hover pointer is removed after ~3.5 seconds, this ensures that all events are received
XCTestExpectation* sleepExpectation = [self expectationWithDescription:@"never fires"];
sleepExpectation.inverted = true;
[self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
// The hover events are interspersed with the right-click events in a varying order.
// Ensure the individual orderings are respected without hardcoding the absolute sequence.
NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
[[NSMutableDictionary alloc] init];
for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
NSString* rawMessage = element.value;
// Parse out the sequence number
NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
NSInteger messageSequenceNumber =
[rawMessage substringWithRange:NSMakeRange(0, commaIndex)].integerValue;
// Parse out the rest of the message
NSString* message = [rawMessage
substringWithRange:NSMakeRange(commaIndex + 1, rawMessage.length - (commaIndex + 1))];
NSMutableArray<NSNumber*>* messageSequenceNumberList = messages[message];
if (messageSequenceNumberList == nil) {
messageSequenceNumberList = [[NSMutableArray alloc] init];
messages[message] = messageSequenceNumberList;
}
[messageSequenceNumberList addObject:@(messageSequenceNumber)];
}
// The number of hover events is not consistent, there could be one or many.
NSMutableArray<NSNumber*>* hoverSequenceNumbers =
messages[@"PointerChange.hover,device=1,buttons=0"];
int hoverRemovedSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.remove,device=1,buttons=0");
// Right click should have buttons = 2
int rightClickAddedSequenceNumber;
int rightClickDownSequenceNumber;
int rightClickUpSequenceNumber;
if (messages[@"PointerChange.add,device=2,buttons=0"] == nil) {
// Sometimes the tap pointer has the same device as the right-click (the UITouch is reused)
rightClickAddedSequenceNumber = 0;
rightClickDownSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.down,device=0,buttons=2");
rightClickUpSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.up,device=0,buttons=2");
} else {
rightClickAddedSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.add,device=2,buttons=0");
rightClickDownSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.down,device=2,buttons=2");
rightClickUpSequenceNumber =
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.up,device=2,buttons=2");
}
XCTAssertGreaterThan(rightClickDownSequenceNumber, rightClickAddedSequenceNumber,
@"Right-click pointer was pressed before it was added");
XCTAssertGreaterThan(rightClickUpSequenceNumber, rightClickDownSequenceNumber,
@"Right-click pointer was released before it was pressed");
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], 4,
@"Hover occured before hover pointer was added");
XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
@"Hover occured after hover pointer was removed");
}
- (void)testPointerHover {
BOOL supportsPointerInteraction = NO;
SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
supportsPointerInteraction =
performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
}
XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
XCUIApplication* app = [[XCUIApplication alloc] init];
app.launchArguments = @[ @"--pointer-events" ];
[app launch];
NSPredicate* predicateToFindFlutterView =
[NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
elementMatchingPredicate:predicateToFindFlutterView];
if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
NSLog(@"%@", app.debugDescription);
XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
@(kSecondsToWaitForFlutterView));
}
XCTAssertNotNil(flutterView);
SEL hover = @selector(hover);
XCTAssertTrue([flutterView respondsToSelector:hover],
@"If supportsPointerInteraction is true, this should be true too.");
[flutterView performSelector:hover];
XCTAssertTrue(
[app.textFields[@"0,PointerChange.add,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.add event did not occur for a hover");
XCTAssertTrue(
[app.textFields[@"1,PointerChange.hover,device=0,buttons=0"] waitForExistenceWithTimeout:1],
@"PointerChange.hover event did not occur for a hover");
// The number of hover events fired is not always the same
NSInteger lastHoverSequenceNumber = -1;
NSPredicate* predicateToFindHoverEvents =
[NSPredicate predicateWithFormat:@"value ENDSWITH ',PointerChange.hover,device=0,buttons=0'"];
for (XCUIElement* textField in
[[app.textFields matchingPredicate:predicateToFindHoverEvents] allElementsBoundByIndex]) {
NSInteger messageSequenceNumber =
[[textField.value componentsSeparatedByString:@","] firstObject].integerValue;
if (messageSequenceNumber > lastHoverSequenceNumber) {
lastHoverSequenceNumber = messageSequenceNumber;
}
}
XCTAssertNotEqual(lastHoverSequenceNumber, -1,
@"PointerChange.hover event did not occur for a hover");
NSString* removeMessage = [NSString
stringWithFormat:@"%ld,PointerChange.remove,device=0,buttons=0", lastHoverSequenceNumber + 1];
XCTAssertTrue([app.textFields[removeMessage] waitForExistenceWithTimeout:1],
@"PointerChange.remove event did not occur for a hover");
}
- (void)testPointerScroll {
BOOL supportsPointerInteraction = NO;
SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
supportsPointerInteraction =
performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
}
XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
XCUIApplication* app = [[XCUIApplication alloc] init];
app.launchArguments = @[ @"--pointer-events" ];
[app launch];
NSPredicate* predicateToFindFlutterView =
[NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
elementMatchingPredicate:predicateToFindFlutterView];
if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
@(kSecondsToWaitForFlutterView));
}
XCTAssertNotNil(flutterView);
SEL scroll = @selector(scrollByDeltaX:deltaY:);
XCTAssertTrue([flutterView respondsToSelector:scroll],
@"If supportsPointerInteraction is true, this should be true too.");
// Need to use NSInvocation in order to send primitive arguments to selector.
NSInvocation* invocation = [NSInvocation
invocationWithMethodSignature:[XCUIElement instanceMethodSignatureForSelector:scroll]];
[invocation setSelector:scroll];
CGFloat deltaX = 0.0;
CGFloat deltaY = 1000.0;
[invocation setArgument:&deltaX atIndex:2];
[invocation setArgument:&deltaY atIndex:3];
[invocation invokeWithTarget:flutterView];
// Hover immediately following the scroll to cause an inertia cancel event.
SEL hover = @selector(hover:);
XCTAssertTrue([flutterView respondsToSelector:hover],
@"If supportsPointerInteraction is true, this should be true too.");
[flutterView performSelector:hover];
// The hover pointer is observed to be removed by the system after ~3.5 seconds of inactivity.
// While this is not a documented behavior, it is the only way to test for the removal of the
// hover pointer. Waiting for 5 seconds will ensure that all events are received before
// processing.
XCTestExpectation* sleepExpectation = [self expectationWithDescription:@"never fires"];
sleepExpectation.inverted = true;
[self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
// There are hover events interspersed with the scroll events in a varying order.
// Ensure the individual orderings are respected without hardcoding the absolute sequence.
NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
[[NSMutableDictionary alloc] init];
for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
NSString* rawMessage = element.value;
// Parse out the sequence number
NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
NSInteger messageSequenceNumber =
[rawMessage substringWithRange:NSMakeRange(0, commaIndex)].integerValue;
// Parse out the rest of the message
NSString* message = [rawMessage
substringWithRange:NSMakeRange(commaIndex + 1, rawMessage.length - (commaIndex + 1))];
NSMutableArray<NSNumber*>* messageSequenceNumberList = messages[message];
if (messageSequenceNumberList == nil) {
messageSequenceNumberList = [[NSMutableArray alloc] init];
messages[message] = messageSequenceNumberList;
}
[messageSequenceNumberList addObject:@(messageSequenceNumber)];
}
// The number of hover events is not consistent, there could be one or many.
int hoverAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages, @"PointerChange.add,device=0,buttons=0,signalKind=PointerSignalKind.none");
NSMutableArray<NSNumber*>* hoverSequenceNumbers =
messages[@"PointerChange.hover,device=0,buttons=0,signalKind=PointerSignalKind.none"];
int hoverRemovedSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages, @"PointerChange.remove,device=0,buttons=0,signalKind=PointerSignalKind.none");
int panZoomAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages, @"PointerChange.add,device=1,buttons=0,signalKind=PointerSignalKind.none");
int panZoomStartSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages, @"PointerChange.panZoomStart,device=1,buttons=0,signalKind=PointerSignalKind.none");
// The number of pan/zoom update events is not consistent, there could be one or many.
NSMutableArray<NSNumber*>* panZoomUpdateSequenceNumbers =
messages[@"PointerChange.panZoomUpdate,device=1,buttons=0,signalKind=PointerSignalKind.none"];
int panZoomEndSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages, @"PointerChange.panZoomEnd,device=1,buttons=0,signalKind=PointerSignalKind.none");
int inertiaCancelSequenceNumber = assertOneMessageAndGetSequenceNumber(
messages,
@"PointerChange.cancel,device=2,buttons=0,signalKind=PointerSignalKind.scrollInertiaCancel");
XCTAssertGreaterThan(panZoomStartSequenceNumber, panZoomAddedSequenceNumber,
@"PanZoomStart occured before pointer was added");
XCTAssertGreaterThan([[panZoomUpdateSequenceNumbers firstObject] intValue],
panZoomStartSequenceNumber, @"PanZoomUpdate occured before PanZoomStart");
XCTAssertGreaterThan(panZoomEndSequenceNumber,
[[panZoomUpdateSequenceNumbers lastObject] intValue],
@"PanZoomUpdate occured after PanZoomEnd");
XCTAssertGreaterThan(inertiaCancelSequenceNumber, panZoomEndSequenceNumber,
@"ScrollInertiaCancel occured before PanZoomEnd");
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], hoverAddedSequenceNumber,
@"Hover occured before pointer was added");
XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
@"Hover occured after pointer was removed");
}
#pragma clang diagnostic pop
@end