-
Notifications
You must be signed in to change notification settings - Fork 6k
Remove iPadOS mouse pointer if no longer connected #28319
Changes from all commits
bc964cf
29185ad
3845191
eeb5e97
fa6b846
7d9c4fd
2b26b23
821c4b5
1752ef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,13 @@ static BOOL performBoolSelector(id target, SEL selector) { | |
| 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" | ||
|
|
@@ -45,11 +52,7 @@ - (void)testPointerButtons { | |
| [app launch]; | ||
|
|
||
| NSPredicate* predicateToFindFlutterView = | ||
| [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, | ||
| NSDictionary<NSString*, id>* _Nullable bindings) { | ||
| XCUIElement* element = evaluatedObject; | ||
| return [element.identifier hasPrefix:@"flutter_view"]; | ||
| }]; | ||
| [NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"]; | ||
| XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny] | ||
| elementMatchingPredicate:predicateToFindFlutterView]; | ||
| if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) { | ||
|
|
@@ -62,23 +65,136 @@ - (void)testPointerButtons { | |
|
|
||
| [flutterView tap]; | ||
| // Initial add event should have buttons = 0 | ||
| XCTAssertTrue([app.textFields[@"PointerChange.add:0"] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.add event did not occur"); | ||
| 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[@"PointerChange.down:0"] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.down event did not occur for a normal tap"); | ||
| XCTAssertTrue([app.textFields[@"PointerChange.up:0"] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.up event did not occur for a normal tap"); | ||
| 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"); | ||
| SEL rightClick = @selector(rightClick); | ||
| XCTAssertTrue([flutterView respondsToSelector:rightClick], | ||
| @"If supportsPointerInteraction is true, this should be true too."); | ||
| [flutterView performSelector:rightClick]; | ||
| // Since each touch is its own device, we can't distinguish the other add event(s) | ||
| // On simulated right click, a hover also occurs, so the hover pointer is added | ||
| XCTAssertTrue( | ||
| [app.textFields[@"3,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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This section is hard to understand, what is it doing, exactly? Does
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just need the first component, I don't want to split the whole string by commas since there are more commas later. |
||
| 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 | ||
| XCTAssertTrue([app.textFields[@"PointerChange.down:2"] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.down event did not occur for a right-click"); | ||
| XCTAssertTrue([app.textFields[@"PointerChange.up:2"] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.up event did not occur for a right-click"); | ||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test generally may be easier to read as a series of expectations, with ordering either enforced or not depending on what you're checking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I observed a lot of different permutations of event order, even though this is somewhat convoluted I don't think the alternative is better. |
||
| 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], 3, | ||
| @"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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initializing to |
||
| 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:@"%d,PointerChange.remove,device=0,buttons=0", lastHoverSequenceNumber + 1]; | ||
| XCTAssertTrue([app.textFields[removeMessage] waitForExistenceWithTimeout:1], | ||
| @"PointerChange.remove event did not occur for a hover"); | ||
| } | ||
| #pragma clang diagnostic pop | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.