Flutter Engine
The Flutter Engine
iPadGestureTests.m
Go to the documentation of this file.
1// Copyright 2020 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import <XCTest/XCTest.h>
6
7static const NSInteger kSecondsToWaitForFlutterView = 30;
8
9@interface iPadGestureTests : XCTestCase
10
11@end
12
13@implementation iPadGestureTests
14
15- (void)setUp {
16 [super setUp];
17 self.continueAfterFailure = NO;
18}
19
20static BOOL performBoolSelector(id target, SEL selector) {
21 NSInvocation* invocation = [NSInvocation
22 invocationWithMethodSignature:[[target class] instanceMethodSignatureForSelector:selector]];
23 [invocation setSelector:selector];
24 [invocation setTarget:target];
25 [invocation invoke];
26 BOOL returnValue;
27 [invocation getReturnValue:&returnValue];
28 return returnValue;
29}
30
31static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, NSString* message) {
32 NSMutableArray<NSNumber*>* matchingMessages = messages[message];
33 XCTAssertNotNil(matchingMessages, @"Did not receive \"%@\" message", message);
34 XCTAssertEqual(matchingMessages.count, 1, @"More than one \"%@\" message", message);
35 return matchingMessages.firstObject.intValue;
36}
37
38// TODO(85810): Remove reflection in this test when Xcode version is upgraded to 13.
39#pragma clang diagnostic push
40#pragma clang diagnostic ignored "-Wundeclared-selector"
41#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
42- (void)testPointerButtons {
43 BOOL supportsPointerInteraction = NO;
44 SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
45 if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
46 supportsPointerInteraction =
47 performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
48 }
49 XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
50 XCUIApplication* app = [[XCUIApplication alloc] init];
51 app.launchArguments = @[ @"--pointer-events" ];
52 [app launch];
53
54 NSPredicate* predicateToFindFlutterView =
55 [NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
56 XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
57 elementMatchingPredicate:predicateToFindFlutterView];
58 if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
59 NSLog(@"%@", app.debugDescription);
60 XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
62 }
63
64 XCTAssertNotNil(flutterView);
65
66 [flutterView tap];
67 // Initial add event should have buttons = 0
68 XCTAssertTrue(
69 [app.textFields[@"0,PointerChange.add,device=0,buttons=0"] waitForExistenceWithTimeout:1],
70 @"PointerChange.add event did not occur for a normal tap");
71 // Normal tap should have buttons = 0, the flutter framework will ensure it has buttons = 1
72 XCTAssertTrue(
73 [app.textFields[@"1,PointerChange.down,device=0,buttons=0"] waitForExistenceWithTimeout:1],
74 @"PointerChange.down event did not occur for a normal tap");
75 XCTAssertTrue(
76 [app.textFields[@"2,PointerChange.up,device=0,buttons=0"] waitForExistenceWithTimeout:1],
77 @"PointerChange.up event did not occur for a normal tap");
78 XCTAssertTrue(
79 [app.textFields[@"3,PointerChange.remove,device=0,buttons=0"] waitForExistenceWithTimeout:1],
80 @"PointerChange.remove event did not occur for a normal tap");
81 SEL rightClick = @selector(rightClick);
82 XCTAssertTrue([flutterView respondsToSelector:rightClick],
83 @"If supportsPointerInteraction is true, this should be true too.");
84 [flutterView performSelector:rightClick];
85 // On simulated right click, a hover also occurs, so the hover pointer is added
86 XCTAssertTrue(
87 [app.textFields[@"4,PointerChange.add,device=1,buttons=0"] waitForExistenceWithTimeout:1],
88 @"PointerChange.add event did not occur for a right-click's hover pointer");
89
90 // The hover pointer is removed after ~3.5 seconds, this ensures that all events are received
91 XCTestExpectation* sleepExpectation = [self expectationWithDescription:@"never fires"];
92 sleepExpectation.inverted = true;
93 [self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
94
95 // The hover events are interspersed with the right-click events in a varying order.
96 // Ensure the individual orderings are respected without hardcoding the absolute sequence.
97 NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
98 [[NSMutableDictionary alloc] init];
99 for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
100 NSString* rawMessage = element.value;
101 // Parse out the sequence number
102 NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
103 NSInteger messageSequenceNumber =
104 [rawMessage substringWithRange:NSMakeRange(0, commaIndex)].integerValue;
105 // Parse out the rest of the message
106 NSString* message = [rawMessage
107 substringWithRange:NSMakeRange(commaIndex + 1, rawMessage.length - (commaIndex + 1))];
108 NSMutableArray<NSNumber*>* messageSequenceNumberList = messages[message];
109 if (messageSequenceNumberList == nil) {
110 messageSequenceNumberList = [[NSMutableArray alloc] init];
111 messages[message] = messageSequenceNumberList;
112 }
113 [messageSequenceNumberList addObject:@(messageSequenceNumber)];
114 }
115 // The number of hover events is not consistent, there could be one or many.
116 NSMutableArray<NSNumber*>* hoverSequenceNumbers =
117 messages[@"PointerChange.hover,device=1,buttons=0"];
118 int hoverRemovedSequenceNumber =
119 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.remove,device=1,buttons=0");
120 // Right click should have buttons = 2
121 int rightClickAddedSequenceNumber;
122 int rightClickDownSequenceNumber;
123 int rightClickUpSequenceNumber;
124 if (messages[@"PointerChange.add,device=2,buttons=0"] == nil) {
125 // Sometimes the tap pointer has the same device as the right-click (the UITouch is reused)
126 rightClickAddedSequenceNumber = 0;
127 rightClickDownSequenceNumber =
128 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.down,device=0,buttons=2");
129 rightClickUpSequenceNumber =
130 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.up,device=0,buttons=2");
131 } else {
132 rightClickAddedSequenceNumber =
133 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.add,device=2,buttons=0");
134 rightClickDownSequenceNumber =
135 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.down,device=2,buttons=2");
136 rightClickUpSequenceNumber =
137 assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.up,device=2,buttons=2");
138 }
139 XCTAssertGreaterThan(rightClickDownSequenceNumber, rightClickAddedSequenceNumber,
140 @"Right-click pointer was pressed before it was added");
141 XCTAssertGreaterThan(rightClickUpSequenceNumber, rightClickDownSequenceNumber,
142 @"Right-click pointer was released before it was pressed");
143 XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], 4,
144 @"Hover occured before hover pointer was added");
145 XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
146 @"Hover occured after hover pointer was removed");
147}
148
149- (void)testPointerHover {
150 BOOL supportsPointerInteraction = NO;
151 SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
152 if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
153 supportsPointerInteraction =
154 performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
155 }
156 XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
157 XCUIApplication* app = [[XCUIApplication alloc] init];
158 app.launchArguments = @[ @"--pointer-events" ];
159 [app launch];
160
161 NSPredicate* predicateToFindFlutterView =
162 [NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
163 XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
164 elementMatchingPredicate:predicateToFindFlutterView];
165 if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
166 NSLog(@"%@", app.debugDescription);
167 XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
169 }
170
171 XCTAssertNotNil(flutterView);
172
173 SEL hover = @selector(hover);
174 XCTAssertTrue([flutterView respondsToSelector:hover],
175 @"If supportsPointerInteraction is true, this should be true too.");
176 [flutterView performSelector:hover];
177 XCTAssertTrue(
178 [app.textFields[@"0,PointerChange.add,device=0,buttons=0"] waitForExistenceWithTimeout:1],
179 @"PointerChange.add event did not occur for a hover");
180 XCTAssertTrue(
181 [app.textFields[@"1,PointerChange.hover,device=0,buttons=0"] waitForExistenceWithTimeout:1],
182 @"PointerChange.hover event did not occur for a hover");
183 // The number of hover events fired is not always the same
184 NSInteger lastHoverSequenceNumber = -1;
185 NSPredicate* predicateToFindHoverEvents =
186 [NSPredicate predicateWithFormat:@"value ENDSWITH ',PointerChange.hover,device=0,buttons=0'"];
187 for (XCUIElement* textField in
188 [[app.textFields matchingPredicate:predicateToFindHoverEvents] allElementsBoundByIndex]) {
189 NSInteger messageSequenceNumber =
190 [[textField.value componentsSeparatedByString:@","] firstObject].integerValue;
191 if (messageSequenceNumber > lastHoverSequenceNumber) {
192 lastHoverSequenceNumber = messageSequenceNumber;
193 }
194 }
195 XCTAssertNotEqual(lastHoverSequenceNumber, -1,
196 @"PointerChange.hover event did not occur for a hover");
197 NSString* removeMessage = [NSString
198 stringWithFormat:@"%ld,PointerChange.remove,device=0,buttons=0", lastHoverSequenceNumber + 1];
199 XCTAssertTrue([app.textFields[removeMessage] waitForExistenceWithTimeout:1],
200 @"PointerChange.remove event did not occur for a hover");
201}
202
203- (void)testPointerScroll {
204 BOOL supportsPointerInteraction = NO;
205 SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
206 if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
207 supportsPointerInteraction =
208 performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
209 }
210 XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
211 XCUIApplication* app = [[XCUIApplication alloc] init];
212 app.launchArguments = @[ @"--pointer-events" ];
213 [app launch];
214
215 NSPredicate* predicateToFindFlutterView =
216 [NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
217 XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
218 elementMatchingPredicate:predicateToFindFlutterView];
219 if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
220 XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
222 }
223
224 XCTAssertNotNil(flutterView);
225
226 SEL scroll = @selector(scrollByDeltaX:deltaY:);
227 XCTAssertTrue([flutterView respondsToSelector:scroll],
228 @"If supportsPointerInteraction is true, this should be true too.");
229 // Need to use NSInvocation in order to send primitive arguments to selector.
230 NSInvocation* invocation = [NSInvocation
231 invocationWithMethodSignature:[XCUIElement instanceMethodSignatureForSelector:scroll]];
232 [invocation setSelector:scroll];
233 CGFloat deltaX = 0.0;
234 CGFloat deltaY = 1000.0;
235 [invocation setArgument:&deltaX atIndex:2];
236 [invocation setArgument:&deltaY atIndex:3];
237 [invocation invokeWithTarget:flutterView];
238
239 // Hover immediately following the scroll to cause an inertia cancel event.
240 SEL hover = @selector(hover:);
241 XCTAssertTrue([flutterView respondsToSelector:hover],
242 @"If supportsPointerInteraction is true, this should be true too.");
243 [flutterView performSelector:hover];
244
245 // The hover pointer is observed to be removed by the system after ~3.5 seconds of inactivity.
246 // While this is not a documented behavior, it is the only way to test for the removal of the
247 // hover pointer. Waiting for 5 seconds will ensure that all events are received before
248 // processing.
249 XCTestExpectation* sleepExpectation = [self expectationWithDescription:@"never fires"];
250 sleepExpectation.inverted = true;
251 [self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
252
253 // There are hover events interspersed with the scroll events in a varying order.
254 // Ensure the individual orderings are respected without hardcoding the absolute sequence.
255 NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
256 [[NSMutableDictionary alloc] init];
257 for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
258 NSString* rawMessage = element.value;
259 // Parse out the sequence number
260 NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
261 NSInteger messageSequenceNumber =
262 [rawMessage substringWithRange:NSMakeRange(0, commaIndex)].integerValue;
263 // Parse out the rest of the message
264 NSString* message = [rawMessage
265 substringWithRange:NSMakeRange(commaIndex + 1, rawMessage.length - (commaIndex + 1))];
266 NSMutableArray<NSNumber*>* messageSequenceNumberList = messages[message];
267 if (messageSequenceNumberList == nil) {
268 messageSequenceNumberList = [[NSMutableArray alloc] init];
269 messages[message] = messageSequenceNumberList;
270 }
271 [messageSequenceNumberList addObject:@(messageSequenceNumber)];
272 }
273 // The number of hover events is not consistent, there could be one or many.
274 int hoverAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
275 messages, @"PointerChange.add,device=0,buttons=0,signalKind=PointerSignalKind.none");
276 NSMutableArray<NSNumber*>* hoverSequenceNumbers =
277 messages[@"PointerChange.hover,device=0,buttons=0,signalKind=PointerSignalKind.none"];
278 int hoverRemovedSequenceNumber = assertOneMessageAndGetSequenceNumber(
279 messages, @"PointerChange.remove,device=0,buttons=0,signalKind=PointerSignalKind.none");
280 int panZoomAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
281 messages, @"PointerChange.add,device=1,buttons=0,signalKind=PointerSignalKind.none");
282 int panZoomStartSequenceNumber = assertOneMessageAndGetSequenceNumber(
283 messages, @"PointerChange.panZoomStart,device=1,buttons=0,signalKind=PointerSignalKind.none");
284 // The number of pan/zoom update events is not consistent, there could be one or many.
285 NSMutableArray<NSNumber*>* panZoomUpdateSequenceNumbers =
286 messages[@"PointerChange.panZoomUpdate,device=1,buttons=0,signalKind=PointerSignalKind.none"];
287 int panZoomEndSequenceNumber = assertOneMessageAndGetSequenceNumber(
288 messages, @"PointerChange.panZoomEnd,device=1,buttons=0,signalKind=PointerSignalKind.none");
289 int inertiaCancelSequenceNumber = assertOneMessageAndGetSequenceNumber(
290 messages,
291 @"PointerChange.cancel,device=2,buttons=0,signalKind=PointerSignalKind.scrollInertiaCancel");
292
293 XCTAssertGreaterThan(panZoomStartSequenceNumber, panZoomAddedSequenceNumber,
294 @"PanZoomStart occured before pointer was added");
295 XCTAssertGreaterThan([[panZoomUpdateSequenceNumbers firstObject] intValue],
296 panZoomStartSequenceNumber, @"PanZoomUpdate occured before PanZoomStart");
297 XCTAssertGreaterThan(panZoomEndSequenceNumber,
298 [[panZoomUpdateSequenceNumbers lastObject] intValue],
299 @"PanZoomUpdate occured after PanZoomEnd");
300 XCTAssertGreaterThan(inertiaCancelSequenceNumber, panZoomEndSequenceNumber,
301 @"ScrollInertiaCancel occured before PanZoomEnd");
302
303 XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], hoverAddedSequenceNumber,
304 @"Hover occured before pointer was added");
305 XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
306 @"Hover occured after pointer was removed");
307}
308#pragma clang diagnostic pop
309
310@end
uint32_t * target
static const NSInteger kSecondsToWaitForFlutterView
static BOOL performBoolSelector(id target, SEL selector)
static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary *messages, NSString *message)
Win32Message message
int BOOL
Definition: windows_types.h:37