Flutter Engine
 
Loading...
Searching...
No Matches
FlutterViewControllerTest.mm
Go to the documentation of this file.
1// Copyright 2013 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
8
9#import <OCMock/OCMock.h>
10
23
24#pragma mark - Test Helper Classes
25
28
29// A wrap to convert FlutterKeyEvent to a ObjC class.
30@interface KeyEventWrapper : NSObject
31@property(nonatomic) FlutterKeyEvent* data;
32- (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event;
33@end
34
35@implementation KeyEventWrapper
36- (instancetype)initWithEvent:(const FlutterKeyEvent*)event {
37 self = [super init];
38 _data = new FlutterKeyEvent(*event);
39 return self;
40}
41
42- (void)dealloc {
43 delete _data;
44}
45@end
46
47/// Responder wrapper that forwards key events to another responder. This is a necessary middle step
48/// for mocking responder because when setting the responder to controller AppKit will access ivars
49/// of the objects, which means it must extend NSResponder instead of just implementing the
50/// selectors.
51@interface FlutterResponderWrapper : NSResponder {
52 NSResponder* _responder;
53}
54@end
55
56@implementation FlutterResponderWrapper
57
58- (instancetype)initWithResponder:(NSResponder*)responder {
59 if (self = [super init]) {
60 _responder = responder;
61 }
62 return self;
63}
64
65- (void)keyDown:(NSEvent*)event {
66 [_responder keyDown:event];
67}
68
69- (void)keyUp:(NSEvent*)event {
70 [_responder keyUp:event];
71}
72
73- (BOOL)performKeyEquivalent:(NSEvent*)event {
74 return [_responder performKeyEquivalent:event];
75}
76
77- (void)flagsChanged:(NSEvent*)event {
78 [_responder flagsChanged:event];
79}
80
81@end
82
83// A FlutterViewController subclass for testing that mouseDown/mouseUp get called when
84// mouse events are sent to the associated view.
86@property(nonatomic, assign) BOOL mouseDownCalled;
87@property(nonatomic, assign) BOOL mouseUpCalled;
88@end
89
91- (void)mouseDown:(NSEvent*)event {
93}
94
95- (void)mouseUp:(NSEvent*)event {
96 self.mouseUpCalled = YES;
97}
98@end
99
100@interface FlutterViewControllerTestObjC : NSObject
101- (bool)testKeyEventsAreSentToFramework:(id)mockEngine;
102- (bool)testKeyEventsArePropagatedIfNotHandled:(id)mockEngine;
103- (bool)testKeyEventsAreNotPropagatedIfHandled:(id)mockEngine;
104- (bool)testCtrlTabKeyEventIsPropagated:(id)mockEngine;
105- (bool)testKeyEquivalentIsPassedToTextInputPlugin:(id)mockEngine;
106- (bool)testFlagsChangedEventsArePropagatedIfNotHandled:(id)mockEngine;
107- (bool)testKeyboardIsRestartedOnEngineRestart:(id)mockEngine;
108- (bool)testTrackpadGesturesAreSentToFramework:(id)mockEngine;
109- (bool)mouseAndGestureEventsAreHandledSeparately:(id)engineMock;
110- (bool)testMouseDownUpEventsSentToNextResponder:(id)mockEngine;
111- (bool)testModifierKeysAreSynthesizedOnMouseMove:(id)mockEngine;
112- (bool)testViewWillAppearCalledMultipleTimes:(id)mockEngine;
113- (bool)testFlutterViewIsConfigured:(id)mockEngine;
114- (bool)testLookupKeyAssets;
115- (bool)testLookupKeyAssetsWithPackage;
116- (bool)testViewControllerIsReleased;
117
118+ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
119 callback:(nullable FlutterKeyEventCallback)callback
120 userData:(nullable void*)userData;
121@end
122
123#pragma mark - Static helper functions
124
125using namespace ::flutter::testing::keycodes;
126
127namespace flutter::testing {
128
129namespace {
130
131id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification, double rotation) {
132 id event = [OCMockObject mockForClass:[NSEvent class]];
133 NSPoint locationInWindow = NSMakePoint(0, 0);
134 CGFloat deltaX = 0;
135 CGFloat deltaY = 0;
136 NSTimeInterval timestamp = 1;
137 NSUInteger modifierFlags = 0;
138 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(type)] type];
139 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(phase)] phase];
140 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(locationInWindow)] locationInWindow];
141 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(deltaX)] deltaX];
142 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(deltaY)] deltaY];
143 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(timestamp)] timestamp];
144 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(modifierFlags)] modifierFlags];
145 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(magnification)] magnification];
146 [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(rotation)] rotation];
147 return event;
148}
149
150// Allocates and returns an engine configured for the test fixture resource configuration.
151FlutterEngine* CreateTestEngine() {
152 NSString* fixtures = @(testing::GetFixturesPath());
153 FlutterDartProject* project = [[FlutterDartProject alloc]
154 initWithAssetsPath:fixtures
155 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
156 return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true];
157}
158
159NSResponder* mockResponder() {
160 NSResponder* mock = OCMStrictClassMock([NSResponder class]);
161 OCMStub([mock keyDown:[OCMArg any]]).andDo(nil);
162 OCMStub([mock keyUp:[OCMArg any]]).andDo(nil);
163 OCMStub([mock flagsChanged:[OCMArg any]]).andDo(nil);
164 return mock;
165}
166
167NSEvent* CreateMouseEvent(NSEventModifierFlags modifierFlags) {
168 return [NSEvent mouseEventWithType:NSEventTypeMouseMoved
169 location:NSZeroPoint
170 modifierFlags:modifierFlags
171 timestamp:0
172 windowNumber:0
173 context:nil
174 eventNumber:0
175 clickCount:1
176 pressure:1.0];
177}
178
179} // namespace
180
181#pragma mark - gtest tests
182
183// Test-specific names for AutoreleasePoolTest, MockFlutterEngineTest fixtures.
186
187TEST_F(FlutterViewControllerTest, HasViewThatHidesOtherViewsInAccessibility) {
188 FlutterViewController* viewControllerMock = CreateMockViewController();
189
190 [viewControllerMock loadView];
191 auto subViews = [viewControllerMock.view subviews];
192
193 EXPECT_EQ([subViews count], 1u);
194 EXPECT_EQ(subViews[0], viewControllerMock.flutterView);
195
196 NSTextField* textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)];
197 [viewControllerMock.view addSubview:textField];
198
199 subViews = [viewControllerMock.view subviews];
200 EXPECT_EQ([subViews count], 2u);
201
202 auto accessibilityChildren = viewControllerMock.view.accessibilityChildren;
203 // The accessibilityChildren should only contains the FlutterView.
204 EXPECT_EQ([accessibilityChildren count], 1u);
205 EXPECT_EQ(accessibilityChildren[0], viewControllerMock.flutterView);
206}
207
208TEST_F(FlutterViewControllerTest, FlutterViewAcceptsFirstMouse) {
209 FlutterViewController* viewControllerMock = CreateMockViewController();
210 [viewControllerMock loadView];
211 EXPECT_EQ([viewControllerMock.flutterView acceptsFirstMouse:nil], YES);
212}
213
214TEST_F(FlutterViewControllerTest, ReparentsPluginWhenAccessibilityDisabled) {
215 FlutterEngine* engine = CreateTestEngine();
216 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
217 nibName:nil
218 bundle:nil];
219 [viewController loadView];
220 // Creates a NSWindow so that sub view can be first responder.
221 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
222 styleMask:NSBorderlessWindowMask
223 backing:NSBackingStoreBuffered
224 defer:NO];
225 window.contentView = viewController.view;
226 NSView* dummyView = [[NSView alloc] initWithFrame:CGRectZero];
227 [viewController.view addSubview:dummyView];
228
229 NSDictionary* setClientConfig = @{
230 @"viewId" : @(flutter::kFlutterImplicitViewId),
231 };
232 FlutterMethodCall* methodCall =
233 [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
234 arguments:@[ @(1), setClientConfig ]];
235 FlutterResult result = ^(id result) {
236 };
237 [engine.textInputPlugin handleMethodCall:methodCall result:result];
238
239 // Attaches FlutterTextInputPlugin to the view;
240 [dummyView addSubview:engine.textInputPlugin];
241 // Makes sure the textInputPlugin can be the first responder.
242
243 EXPECT_TRUE([window makeFirstResponder:engine.textInputPlugin]);
244 EXPECT_EQ([window firstResponder], engine.textInputPlugin);
245 EXPECT_FALSE(engine.textInputPlugin.superview == viewController.view);
246 [viewController onAccessibilityStatusChanged:NO];
247 // FlutterView becomes child of view controller
248 EXPECT_TRUE(viewController.engine.textInputPlugin.superview == viewController.view);
249}
250
251TEST_F(FlutterViewControllerTest, CanSetMouseTrackingModeBeforeViewLoaded) {
252 NSString* fixtures = @(testing::GetFixturesPath());
253 FlutterDartProject* project = [[FlutterDartProject alloc]
254 initWithAssetsPath:fixtures
255 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
256 FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
257 viewController.mouseTrackingMode = kFlutterMouseTrackingModeInActiveApp;
258 ASSERT_EQ(viewController.mouseTrackingMode, kFlutterMouseTrackingModeInActiveApp);
259}
260
261TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsAreSentToFramework) {
262 id mockEngine = GetMockEngine();
263 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsAreSentToFramework:mockEngine]);
264}
265
266TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsArePropagatedIfNotHandled) {
267 id mockEngine = GetMockEngine();
268 ASSERT_TRUE(
269 [[FlutterViewControllerTestObjC alloc] testKeyEventsArePropagatedIfNotHandled:mockEngine]);
270}
271
272TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsAreNotPropagatedIfHandled) {
273 id mockEngine = GetMockEngine();
274 ASSERT_TRUE(
275 [[FlutterViewControllerTestObjC alloc] testKeyEventsAreNotPropagatedIfHandled:mockEngine]);
276}
277
278TEST_F(FlutterViewControllerMockEngineTest, TestCtrlTabKeyEventIsPropagated) {
279 id mockEngine = GetMockEngine();
280 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testCtrlTabKeyEventIsPropagated:mockEngine]);
281}
282
283TEST_F(FlutterViewControllerMockEngineTest, TestKeyEquivalentIsPassedToTextInputPlugin) {
284 id mockEngine = GetMockEngine();
285 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc]
286 testKeyEquivalentIsPassedToTextInputPlugin:mockEngine]);
287}
288
289TEST_F(FlutterViewControllerMockEngineTest, TestFlagsChangedEventsArePropagatedIfNotHandled) {
290 id mockEngine = GetMockEngine();
291 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc]
292 testFlagsChangedEventsArePropagatedIfNotHandled:mockEngine]);
293}
294
295TEST_F(FlutterViewControllerMockEngineTest, TestKeyboardIsRestartedOnEngineRestart) {
296 id mockEngine = GetMockEngine();
297 ASSERT_TRUE(
298 [[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart:mockEngine]);
299}
300
301TEST_F(FlutterViewControllerMockEngineTest, TestTrackpadGesturesAreSentToFramework) {
302 id mockEngine = GetMockEngine();
303 ASSERT_TRUE(
304 [[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework:mockEngine]);
305}
306
307TEST_F(FlutterViewControllerMockEngineTest, TestmouseAndGestureEventsAreHandledSeparately) {
308 id mockEngine = GetMockEngine();
309 ASSERT_TRUE(
310 [[FlutterViewControllerTestObjC alloc] mouseAndGestureEventsAreHandledSeparately:mockEngine]);
311}
312
313TEST_F(FlutterViewControllerMockEngineTest, TestMouseDownUpEventsSentToNextResponder) {
314 id mockEngine = GetMockEngine();
315 ASSERT_TRUE(
316 [[FlutterViewControllerTestObjC alloc] testMouseDownUpEventsSentToNextResponder:mockEngine]);
317}
318
319TEST_F(FlutterViewControllerMockEngineTest, TestModifierKeysAreSynthesizedOnMouseMove) {
320 id mockEngine = GetMockEngine();
321 ASSERT_TRUE(
322 [[FlutterViewControllerTestObjC alloc] testModifierKeysAreSynthesizedOnMouseMove:mockEngine]);
323}
324
325TEST_F(FlutterViewControllerMockEngineTest, testViewWillAppearCalledMultipleTimes) {
326 id mockEngine = GetMockEngine();
327 ASSERT_TRUE(
328 [[FlutterViewControllerTestObjC alloc] testViewWillAppearCalledMultipleTimes:mockEngine]);
329}
330
331TEST_F(FlutterViewControllerMockEngineTest, testFlutterViewIsConfigured) {
332 id mockEngine = GetMockEngine();
333 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testFlutterViewIsConfigured:mockEngine]);
334}
335
336TEST_F(FlutterViewControllerTest, testLookupKeyAssets) {
337 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testLookupKeyAssets]);
338}
339
340TEST_F(FlutterViewControllerTest, testLookupKeyAssetsWithPackage) {
341 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testLookupKeyAssetsWithPackage]);
342}
343
344TEST_F(FlutterViewControllerTest, testViewControllerIsReleased) {
345 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testViewControllerIsReleased]);
346}
347
348} // namespace flutter::testing
349
350#pragma mark - FlutterViewControllerTestObjC
351
352@implementation FlutterViewControllerTestObjC
353
354- (bool)testKeyEventsAreSentToFramework:(id)engineMock {
355 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
356 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
357 [engineMock binaryMessenger])
358 .andReturn(binaryMessengerMock);
359 FlutterKeyboardManager* keyboardManager =
360 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
361 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
362
363 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
364 callback:nil
365 userData:nil])
366 .andCall([FlutterViewControllerTestObjC class],
367 @selector(respondFalseForSendEvent:callback:userData:));
368 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
369 nibName:@""
370 bundle:nil];
371 NSDictionary* expectedEvent = @{
372 @"keymap" : @"macos",
373 @"type" : @"keydown",
374 @"keyCode" : @(65),
375 @"modifiers" : @(538968064),
376 @"characters" : @".",
377 @"charactersIgnoringModifiers" : @".",
378 };
379 NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
380 CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
381 NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
382 [viewController viewWillAppear]; // Initializes the event channel.
383 [viewController keyDown:event];
384 @try {
385 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
386 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
387 message:encodedKeyEvent
388 binaryReply:[OCMArg any]]);
389 } @catch (...) {
390 return false;
391 }
392 return true;
393}
394
395// Regression test for https://github.com/flutter/flutter/issues/122084.
396- (bool)testCtrlTabKeyEventIsPropagated:(id)engineMock {
397 __block bool called = false;
398 __block FlutterKeyEvent last_event;
399 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
400 callback:nil
401 userData:nil])
402 .andDo((^(NSInvocation* invocation) {
403 FlutterKeyEvent* event;
404 [invocation getArgument:&event atIndex:2];
405 called = true;
406 last_event = *event;
407 }));
408 FlutterKeyboardManager* keyboardManager =
409 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
410 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
411
412 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
413 nibName:@""
414 bundle:nil];
415 // Ctrl+tab
416 NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
417 location:NSZeroPoint
418 modifierFlags:0x40101
419 timestamp:0
420 windowNumber:0
421 context:nil
422 characters:@""
423 charactersIgnoringModifiers:@""
424 isARepeat:NO
425 keyCode:48];
426 const uint64_t kPhysicalKeyTab = 0x7002b;
427
428 [viewController viewWillAppear]; // Initializes the event channel.
429 // Creates a NSWindow so that FlutterView view can be first responder.
430 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
431 styleMask:NSBorderlessWindowMask
432 backing:NSBackingStoreBuffered
433 defer:NO];
434 window.contentView = viewController.view;
435 [window makeFirstResponder:viewController.flutterView];
436 [viewController.view performKeyEquivalent:event];
437
438 EXPECT_TRUE(called);
439 EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
440 EXPECT_EQ(last_event.physical, kPhysicalKeyTab);
441 return true;
442}
443
444- (bool)testKeyEquivalentIsPassedToTextInputPlugin:(id)engineMock {
445 __block bool called = false;
446 __block FlutterKeyEvent last_event;
447 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
448 callback:nil
449 userData:nil])
450 .andDo((^(NSInvocation* invocation) {
451 FlutterKeyEvent* event;
452 [invocation getArgument:&event atIndex:2];
453 called = true;
454 last_event = *event;
455 }));
456 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
457 nibName:@""
458 bundle:nil];
459 // Ctrl+tab
460 NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
461 location:NSZeroPoint
462 modifierFlags:0x40101
463 timestamp:0
464 windowNumber:0
465 context:nil
466 characters:@""
467 charactersIgnoringModifiers:@""
468 isARepeat:NO
469 keyCode:48];
470 const uint64_t kPhysicalKeyTab = 0x7002b;
471
472 [viewController viewWillAppear]; // Initializes the event channel.
473
474 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
475 styleMask:NSBorderlessWindowMask
476 backing:NSBackingStoreBuffered
477 defer:NO];
478 window.contentView = viewController.view;
479
480 NSDictionary* setClientConfig = @{
481 @"viewId" : @(flutter::kFlutterImplicitViewId),
482 };
483 FlutterMethodCall* methodCall =
484 [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
485 arguments:@[ @(1), setClientConfig ]];
486 FlutterResult result = ^(id result) {
487 };
488 [viewController.engine.textInputPlugin handleMethodCall:methodCall result:result];
489
490 [viewController.engine.textInputPlugin
491 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" arguments:@[]]
492 result:^(id){
493 }];
494
495 // Make the textInputPlugin first responder. This should still result in
496 // view controller reporting the key event.
497 [window makeFirstResponder:viewController.engine.textInputPlugin];
498
499 [viewController.view performKeyEquivalent:event];
500
501 EXPECT_TRUE(called);
502 EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
503 EXPECT_EQ(last_event.physical, kPhysicalKeyTab);
504 return true;
505}
506
507- (bool)testKeyEventsArePropagatedIfNotHandled:(id)engineMock {
508 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
509 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
510 [engineMock binaryMessenger])
511 .andReturn(binaryMessengerMock);
512 FlutterKeyboardManager* keyboardManager =
513 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
514 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
515 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
516 callback:nil
517 userData:nil])
518 .andCall([FlutterViewControllerTestObjC class],
519 @selector(respondFalseForSendEvent:callback:userData:));
520 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
521 nibName:@""
522 bundle:nil];
523 id responderMock = flutter::testing::mockResponder();
524 id responderWrapper = [[FlutterResponderWrapper alloc] initWithResponder:responderMock];
525 viewController.nextResponder = responderWrapper;
526 NSDictionary* expectedEvent = @{
527 @"keymap" : @"macos",
528 @"type" : @"keydown",
529 @"keyCode" : @(65),
530 @"modifiers" : @(538968064),
531 @"characters" : @".",
532 @"charactersIgnoringModifiers" : @".",
533 };
534 NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
535 CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
536 NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
537 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
538 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
539 message:encodedKeyEvent
540 binaryReply:[OCMArg any]])
541 .andDo((^(NSInvocation* invocation) {
543 [invocation getArgument:&handler atIndex:4];
544 NSDictionary* reply = @{
545 @"handled" : @(false),
546 };
547 NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
548 handler(encodedReply);
549 }));
550 [viewController viewWillAppear]; // Initializes the event channel.
551 [viewController keyDown:event];
552 @try {
553 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
554 [responderMock keyDown:[OCMArg any]]);
555 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
556 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
557 message:encodedKeyEvent
558 binaryReply:[OCMArg any]]);
559 } @catch (...) {
560 return false;
561 }
562 return true;
563}
564
565- (bool)testFlutterViewIsConfigured:(id)engineMock {
566 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
567 OCMStub([engineMock renderer]).andReturn(renderer_);
568
569 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
570 nibName:@""
571 bundle:nil];
572 [viewController loadView];
573
574 @try {
575 // Make sure "renderer" was called during "loadView", which means "flutterView" is created
576 OCMVerify([engineMock renderer]);
577 } @catch (...) {
578 return false;
579 }
580
581 return true;
582}
583
584- (bool)testFlagsChangedEventsArePropagatedIfNotHandled:(id)engineMock {
585 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
586 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
587 [engineMock binaryMessenger])
588 .andReturn(binaryMessengerMock);
589 FlutterKeyboardManager* keyboardManager =
590 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
591 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
592 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
593 callback:nil
594 userData:nil])
595 .andCall([FlutterViewControllerTestObjC class],
596 @selector(respondFalseForSendEvent:callback:userData:));
597 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
598 nibName:@""
599 bundle:nil];
600 id responderMock = flutter::testing::mockResponder();
601 id responderWrapper = [[FlutterResponderWrapper alloc] initWithResponder:responderMock];
602 viewController.nextResponder = responderWrapper;
603 NSDictionary* expectedEvent = @{
604 @"keymap" : @"macos",
605 @"type" : @"keydown",
606 @"keyCode" : @(56), // SHIFT key
607 @"modifiers" : @(537001986),
608 };
609 NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
610 CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 56, TRUE); // SHIFT key
611 CGEventSetType(cgEvent, kCGEventFlagsChanged);
612 NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
613 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
614 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
615 message:encodedKeyEvent
616 binaryReply:[OCMArg any]])
617 .andDo((^(NSInvocation* invocation) {
619 [invocation getArgument:&handler atIndex:4];
620 NSDictionary* reply = @{
621 @"handled" : @(false),
622 };
623 NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
624 handler(encodedReply);
625 }));
626 [viewController viewWillAppear]; // Initializes the event channel.
627 [viewController flagsChanged:event];
628 @try {
629 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
630 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
631 message:encodedKeyEvent
632 binaryReply:[OCMArg any]]);
633 } @catch (NSException* e) {
634 NSLog(@"%@", e.reason);
635 return false;
636 }
637 return true;
638}
639
640- (bool)testKeyEventsAreNotPropagatedIfHandled:(id)engineMock {
641 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
642 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
643 [engineMock binaryMessenger])
644 .andReturn(binaryMessengerMock);
645 FlutterKeyboardManager* keyboardManager =
646 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
647 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
648 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
649 callback:nil
650 userData:nil])
651 .andCall([FlutterViewControllerTestObjC class],
652 @selector(respondFalseForSendEvent:callback:userData:));
653 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
654 nibName:@""
655 bundle:nil];
656 id responderMock = flutter::testing::mockResponder();
657 id responderWrapper = [[FlutterResponderWrapper alloc] initWithResponder:responderMock];
658 viewController.nextResponder = responderWrapper;
659 NSDictionary* expectedEvent = @{
660 @"keymap" : @"macos",
661 @"type" : @"keydown",
662 @"keyCode" : @(65),
663 @"modifiers" : @(538968064),
664 @"characters" : @".",
665 @"charactersIgnoringModifiers" : @".",
666 };
667 NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
668 CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
669 NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
670 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
671 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
672 message:encodedKeyEvent
673 binaryReply:[OCMArg any]])
674 .andDo((^(NSInvocation* invocation) {
676 [invocation getArgument:&handler atIndex:4];
677 NSDictionary* reply = @{
678 @"handled" : @(true),
679 };
680 NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
681 handler(encodedReply);
682 }));
683 [viewController viewWillAppear]; // Initializes the event channel.
684 [viewController keyDown:event];
685 @try {
686 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
687 never(), [responderMock keyDown:[OCMArg any]]);
688 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
689 [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
690 message:encodedKeyEvent
691 binaryReply:[OCMArg any]]);
692 } @catch (...) {
693 return false;
694 }
695 return true;
696}
697
698- (bool)testKeyboardIsRestartedOnEngineRestart:(id)engineMock {
699 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
700 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
701 [engineMock binaryMessenger])
702 .andReturn(binaryMessengerMock);
703 __block bool called = false;
704 __block FlutterKeyEvent last_event;
705 __block FlutterKeyboardManager* keyboardManager =
706 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
707
708 OCMStub([engineMock keyboardManager]).andDo(^(NSInvocation* invocation) {
709 [invocation setReturnValue:&keyboardManager];
710 });
711
712 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
713 callback:nil
714 userData:nil])
715 .andDo((^(NSInvocation* invocation) {
716 FlutterKeyEvent* event;
717 [invocation getArgument:&event atIndex:2];
718 called = true;
719 last_event = *event;
720 }));
721
722 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
723 nibName:@""
724 bundle:nil];
725 [viewController viewWillAppear];
726 NSEvent* keyADown = [NSEvent keyEventWithType:NSEventTypeKeyDown
727 location:NSZeroPoint
728 modifierFlags:0x100
729 timestamp:0
730 windowNumber:0
731 context:nil
732 characters:@"a"
733 charactersIgnoringModifiers:@"a"
734 isARepeat:FALSE
735 keyCode:0];
736 const uint64_t kPhysicalKeyA = 0x70004;
737
738 // Send KeyA key down event twice. Without restarting the keyboard during
739 // onPreEngineRestart, the second event received will be an empty event with
740 // physical key 0x0 because duplicate key down events are ignored.
741
742 called = false;
743 [viewController keyDown:keyADown];
744 EXPECT_TRUE(called);
745 EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
746 EXPECT_EQ(last_event.physical, kPhysicalKeyA);
747
748 keyboardManager = [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
749
750 called = false;
751 [viewController keyDown:keyADown];
752 EXPECT_TRUE(called);
753 EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
754 EXPECT_EQ(last_event.physical, kPhysicalKeyA);
755 return true;
756}
757
758+ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
759 callback:(nullable FlutterKeyEventCallback)callback
760 userData:(nullable void*)userData {
761 if (callback != nullptr) {
762 callback(false, userData);
763 }
764}
765
766- (bool)testTrackpadGesturesAreSentToFramework:(id)engineMock {
767 // Need to return a real renderer to allow view controller to load.
768 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
769 OCMStub([engineMock renderer]).andReturn(renderer_);
770 __block bool called = false;
771 __block FlutterPointerEvent last_event;
772 OCMStub([[engineMock ignoringNonObjectArgs] sendPointerEvent:kDefaultFlutterPointerEvent])
773 .andDo((^(NSInvocation* invocation) {
774 FlutterPointerEvent* event;
775 [invocation getArgument:&event atIndex:2];
776 called = true;
777 last_event = *event;
778 }));
779
780 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
781 nibName:@""
782 bundle:nil];
783 [viewController loadView];
784
785 // Test for pan events.
786 // Start gesture.
787 CGEventRef cgEventStart = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0);
788 CGEventSetType(cgEventStart, kCGEventScrollWheel);
789 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventScrollPhase, kCGScrollPhaseBegan);
790 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventIsContinuous, 1);
791
792 called = false;
793 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventStart]];
794 EXPECT_TRUE(called);
795 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
796 EXPECT_EQ(last_event.phase, kPanZoomStart);
797 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
798 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
799
800 // Update gesture.
801 CGEventRef cgEventUpdate = CGEventCreateCopy(cgEventStart);
802 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventScrollPhase, kCGScrollPhaseChanged);
803 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis2, 1); // pan_x
804 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis1, 2); // pan_y
805
806 called = false;
807 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventUpdate]];
808 EXPECT_TRUE(called);
809 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
810 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
811 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
812 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
813 EXPECT_EQ(last_event.pan_x, 8 * viewController.flutterView.layer.contentsScale);
814 EXPECT_EQ(last_event.pan_y, 16 * viewController.flutterView.layer.contentsScale);
815
816 // Make sure the pan values accumulate.
817 called = false;
818 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventUpdate]];
819 EXPECT_TRUE(called);
820 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
821 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
822 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
823 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
824 EXPECT_EQ(last_event.pan_x, 16 * viewController.flutterView.layer.contentsScale);
825 EXPECT_EQ(last_event.pan_y, 32 * viewController.flutterView.layer.contentsScale);
826
827 // End gesture.
828 CGEventRef cgEventEnd = CGEventCreateCopy(cgEventStart);
829 CGEventSetIntegerValueField(cgEventEnd, kCGScrollWheelEventScrollPhase, kCGScrollPhaseEnded);
830
831 called = false;
832 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventEnd]];
833 EXPECT_TRUE(called);
834 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
835 EXPECT_EQ(last_event.phase, kPanZoomEnd);
836 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
837 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
838
839 // Start system momentum.
840 CGEventRef cgEventMomentumStart = CGEventCreateCopy(cgEventStart);
841 CGEventSetIntegerValueField(cgEventMomentumStart, kCGScrollWheelEventScrollPhase, 0);
842 CGEventSetIntegerValueField(cgEventMomentumStart, kCGScrollWheelEventMomentumPhase,
843 kCGMomentumScrollPhaseBegin);
844
845 called = false;
846 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumStart]];
847 EXPECT_FALSE(called);
848
849 // Advance system momentum.
850 CGEventRef cgEventMomentumUpdate = CGEventCreateCopy(cgEventStart);
851 CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventScrollPhase, 0);
852 CGEventSetIntegerValueField(cgEventMomentumUpdate, kCGScrollWheelEventMomentumPhase,
853 kCGMomentumScrollPhaseContinue);
854
855 called = false;
856 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumUpdate]];
857 EXPECT_FALSE(called);
858
859 // Mock a touch on the trackpad.
860 id touchMock = OCMClassMock([NSTouch class]);
861 NSSet* touchSet = [NSSet setWithObject:touchMock];
862 id touchEventMock1 = OCMClassMock([NSEvent class]);
863 OCMStub([touchEventMock1 allTouches]).andReturn(touchSet);
864 CGPoint touchLocation = {0, 0};
865 OCMStub([touchEventMock1 locationInWindow]).andReturn(touchLocation);
866 OCMStub([(NSEvent*)touchEventMock1 timestamp]).andReturn(0.150); // 150 milliseconds.
867
868 // Scroll inertia cancel event should not be issued (timestamp too far in the future).
869 called = false;
870 [viewController touchesBeganWithEvent:touchEventMock1];
871 EXPECT_FALSE(called);
872
873 // Mock another touch on the trackpad.
874 id touchEventMock2 = OCMClassMock([NSEvent class]);
875 OCMStub([touchEventMock2 allTouches]).andReturn(touchSet);
876 OCMStub([touchEventMock2 locationInWindow]).andReturn(touchLocation);
877 OCMStub([(NSEvent*)touchEventMock2 timestamp]).andReturn(0.005); // 5 milliseconds.
878
879 // Scroll inertia cancel event should be issued.
880 called = false;
881 [viewController touchesBeganWithEvent:touchEventMock2];
882 EXPECT_TRUE(called);
883 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScrollInertiaCancel);
884 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
885
886 // End system momentum.
887 CGEventRef cgEventMomentumEnd = CGEventCreateCopy(cgEventStart);
888 CGEventSetIntegerValueField(cgEventMomentumEnd, kCGScrollWheelEventScrollPhase, 0);
889 CGEventSetIntegerValueField(cgEventMomentumEnd, kCGScrollWheelEventMomentumPhase,
890 kCGMomentumScrollPhaseEnd);
891
892 called = false;
893 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMomentumEnd]];
894 EXPECT_FALSE(called);
895
896 // May-begin and cancel are used while macOS determines which type of gesture to choose.
897 CGEventRef cgEventMayBegin = CGEventCreateCopy(cgEventStart);
898 CGEventSetIntegerValueField(cgEventMayBegin, kCGScrollWheelEventScrollPhase,
899 kCGScrollPhaseMayBegin);
900
901 called = false;
902 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventMayBegin]];
903 EXPECT_TRUE(called);
904 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
905 EXPECT_EQ(last_event.phase, kPanZoomStart);
906 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
907 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
908
909 // Cancel gesture.
910 CGEventRef cgEventCancel = CGEventCreateCopy(cgEventStart);
911 CGEventSetIntegerValueField(cgEventCancel, kCGScrollWheelEventScrollPhase,
912 kCGScrollPhaseCancelled);
913
914 called = false;
915 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventCancel]];
916 EXPECT_TRUE(called);
917 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
918 EXPECT_EQ(last_event.phase, kPanZoomEnd);
919 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
920 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
921
922 // A discrete scroll event should use the PointerSignal system.
923 CGEventRef cgEventDiscrete = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0);
924 CGEventSetType(cgEventDiscrete, kCGEventScrollWheel);
925 CGEventSetIntegerValueField(cgEventDiscrete, kCGScrollWheelEventIsContinuous, 0);
926 CGEventSetIntegerValueField(cgEventDiscrete, kCGScrollWheelEventDeltaAxis2, 1); // scroll_delta_x
927 CGEventSetIntegerValueField(cgEventDiscrete, kCGScrollWheelEventDeltaAxis1, 2); // scroll_delta_y
928
929 called = false;
930 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventDiscrete]];
931 EXPECT_TRUE(called);
932 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScroll);
933 // pixelsPerLine is 40.0 and direction is reversed.
934 EXPECT_EQ(last_event.scroll_delta_x, -40 * viewController.flutterView.layer.contentsScale);
935 EXPECT_EQ(last_event.scroll_delta_y, -80 * viewController.flutterView.layer.contentsScale);
936
937 // A discrete scroll event should use the PointerSignal system, and flip the
938 // direction when shift is pressed.
939 CGEventRef cgEventDiscreteShift =
940 CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0);
941 CGEventSetType(cgEventDiscreteShift, kCGEventScrollWheel);
942 CGEventSetFlags(cgEventDiscreteShift, static_cast<uint64_t>(kCGEventFlagMaskShift) |
943 static_cast<uint64_t>(flutter::kModifierFlagShiftLeft));
944 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventIsContinuous, 0);
945 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventDeltaAxis2,
946 0); // scroll_delta_x
947 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventDeltaAxis1,
948 2); // scroll_delta_y
949
950 called = false;
951 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventDiscreteShift]];
952 EXPECT_TRUE(called);
953 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScroll);
954 // pixelsPerLine is 40.0, direction is reversed and axes have been flipped back.
955 EXPECT_FLOAT_EQ(last_event.scroll_delta_x, 0.0 * viewController.flutterView.layer.contentsScale);
956 EXPECT_FLOAT_EQ(last_event.scroll_delta_y,
957 -80.0 * viewController.flutterView.layer.contentsScale);
958
959 // Test for scale events.
960 // Start gesture.
961 called = false;
962 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
963 NSEventPhaseBegan, 1, 0)];
964 EXPECT_TRUE(called);
965 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
966 EXPECT_EQ(last_event.phase, kPanZoomStart);
967 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
968 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
969
970 // Update gesture.
971 called = false;
972 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
973 NSEventPhaseChanged, 1, 0)];
974 EXPECT_TRUE(called);
975 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
976 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
977 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
978 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
979 EXPECT_EQ(last_event.pan_x, 0);
980 EXPECT_EQ(last_event.pan_y, 0);
981 EXPECT_EQ(last_event.scale, 2); // macOS uses logarithmic scaling values, the linear value for
982 // flutter here should be 2^1 = 2.
983 EXPECT_EQ(last_event.rotation, 0);
984
985 // Make sure the scale values accumulate.
986 called = false;
987 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
988 NSEventPhaseChanged, 1, 0)];
989 EXPECT_TRUE(called);
990 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
991 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
992 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
993 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
994 EXPECT_EQ(last_event.pan_x, 0);
995 EXPECT_EQ(last_event.pan_y, 0);
996 EXPECT_EQ(last_event.scale, 4); // macOS uses logarithmic scaling values, the linear value for
997 // flutter here should be 2^(1+1) = 2.
998 EXPECT_EQ(last_event.rotation, 0);
999
1000 // End gesture.
1001 called = false;
1002 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
1003 NSEventPhaseEnded, 0, 0)];
1004 EXPECT_TRUE(called);
1005 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1006 EXPECT_EQ(last_event.phase, kPanZoomEnd);
1007 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1008 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1009
1010 // Test for rotation events.
1011 // Start gesture.
1012 called = false;
1013 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1014 NSEventPhaseBegan, 1, 0)];
1015 EXPECT_TRUE(called);
1016 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1017 EXPECT_EQ(last_event.phase, kPanZoomStart);
1018 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1019 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1020
1021 // Update gesture.
1022 called = false;
1023 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(
1024 NSEventTypeRotate, NSEventPhaseChanged, 0, -180)]; // degrees
1025 EXPECT_TRUE(called);
1026 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1027 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
1028 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1029 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1030 EXPECT_EQ(last_event.pan_x, 0);
1031 EXPECT_EQ(last_event.pan_y, 0);
1032 EXPECT_EQ(last_event.scale, 1);
1033 EXPECT_EQ(last_event.rotation, M_PI); // radians
1034
1035 // Make sure the rotation values accumulate.
1036 called = false;
1037 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(
1038 NSEventTypeRotate, NSEventPhaseChanged, 0, -360)]; // degrees
1039 EXPECT_TRUE(called);
1040 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1041 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
1042 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1043 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1044 EXPECT_EQ(last_event.pan_x, 0);
1045 EXPECT_EQ(last_event.pan_y, 0);
1046 EXPECT_EQ(last_event.scale, 1);
1047 EXPECT_EQ(last_event.rotation, 3 * M_PI); // radians
1048
1049 // End gesture.
1050 called = false;
1051 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1052 NSEventPhaseEnded, 0, 0)];
1053 EXPECT_TRUE(called);
1054 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1055 EXPECT_EQ(last_event.phase, kPanZoomEnd);
1056 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1057 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1058
1059 // Test that stray NSEventPhaseCancelled event does not crash
1060 called = false;
1061 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1062 NSEventPhaseCancelled, 0, 0)];
1063 EXPECT_FALSE(called);
1064
1065 return true;
1066}
1067
1068// Magic mouse can interleave mouse events with scroll events. This must not crash.
1069- (bool)mouseAndGestureEventsAreHandledSeparately:(id)engineMock {
1070 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1071 nibName:@""
1072 bundle:nil];
1073 [viewController loadView];
1074
1075 // Test for pan events.
1076 // Start gesture.
1077 fml::CFRef<CGEventRef> cgEventStart(
1078 CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0));
1079 CGEventSetType(cgEventStart, kCGEventScrollWheel);
1080 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventScrollPhase, kCGScrollPhaseBegan);
1081 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventIsContinuous, 1);
1082 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventStart]];
1083
1084 fml::CFRef<CGEventRef> cgEventUpdate(CGEventCreateCopy(cgEventStart));
1085 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventScrollPhase, kCGScrollPhaseChanged);
1086 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis2, 1); // pan_x
1087 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis1, 2); // pan_y
1088 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventUpdate]];
1089
1090 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1091 [viewController mouseEntered:mouseEvent];
1092 [viewController mouseExited:mouseEvent];
1093
1094 // End gesture.
1095 fml::CFRef<CGEventRef> cgEventEnd(CGEventCreateCopy(cgEventStart));
1096 CGEventSetIntegerValueField(cgEventEnd, kCGScrollWheelEventScrollPhase, kCGScrollPhaseEnded);
1097 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventEnd]];
1098
1099 return true;
1100}
1101
1102- (bool)testViewWillAppearCalledMultipleTimes:(id)engineMock {
1103 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1104 nibName:@""
1105 bundle:nil];
1106 [viewController viewWillAppear];
1107 [viewController viewWillAppear];
1108 return true;
1109}
1110
1111- (bool)testLookupKeyAssets {
1112 FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:nil];
1113 NSString* key = [viewController lookupKeyForAsset:@"test.png"];
1114 EXPECT_TRUE(
1115 [key isEqualToString:@"Contents/Frameworks/App.framework/Resources/flutter_assets/test.png"]);
1116 return true;
1117}
1118
1119- (bool)testLookupKeyAssetsWithPackage {
1120 FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:nil];
1121
1122 NSString* packageKey = [viewController lookupKeyForAsset:@"test.png" fromPackage:@"test"];
1123 EXPECT_TRUE([packageKey
1124 isEqualToString:
1125 @"Contents/Frameworks/App.framework/Resources/flutter_assets/packages/test/test.png"]);
1126 return true;
1127}
1128
1129static void SwizzledNoop(id self, SEL _cmd) {}
1130
1131// Verify workaround an AppKit bug where mouseDown/mouseUp are not called on the view controller if
1132// the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
1133// setting is enabled.
1134//
1135// See: https://github.com/flutter/flutter/issues/115015
1136// See: http://www.openradar.me/FB12050037
1137// See: https://developer.apple.com/documentation/appkit/nsresponder/1524634-mousedown
1138//
1139// TODO(cbracken): https://github.com/flutter/flutter/issues/154063
1140// Remove this test when we drop support for macOS 12 (Monterey).
1141- (bool)testMouseDownUpEventsSentToNextResponder:(id)engineMock {
1142 if (@available(macOS 13.3.1, *)) {
1143 // This workaround is disabled for macOS 13.3.1 onwards, since the underlying AppKit bug is
1144 // fixed.
1145 return true;
1146 }
1147
1148 // The root cause of the above bug is NSResponder mouseDown/mouseUp methods that don't correctly
1149 // walk the responder chain calling the appropriate method on the next responder under certain
1150 // conditions. Simulate this by swizzling out the default implementations and replacing them with
1151 // no-ops.
1152 Method mouseDown = class_getInstanceMethod([NSResponder class], @selector(mouseDown:));
1153 Method mouseUp = class_getInstanceMethod([NSResponder class], @selector(mouseUp:));
1154 IMP noopImp = (IMP)SwizzledNoop;
1155 IMP origMouseDown = method_setImplementation(mouseDown, noopImp);
1156 IMP origMouseUp = method_setImplementation(mouseUp, noopImp);
1157
1158 // Verify that mouseDown/mouseUp trigger mouseDown/mouseUp calls on FlutterViewController.
1160 [[MouseEventFlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil];
1162
1163 EXPECT_FALSE(viewController.mouseDownCalled);
1164 EXPECT_FALSE(viewController.mouseUpCalled);
1165
1166 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1167 [view mouseDown:mouseEvent];
1168 EXPECT_TRUE(viewController.mouseDownCalled);
1169 EXPECT_FALSE(viewController.mouseUpCalled);
1170
1171 viewController.mouseDownCalled = NO;
1172 [view mouseUp:mouseEvent];
1173 EXPECT_FALSE(viewController.mouseDownCalled);
1174 EXPECT_TRUE(viewController.mouseUpCalled);
1175
1176 // Restore the original NSResponder mouseDown/mouseUp implementations.
1177 method_setImplementation(mouseDown, origMouseDown);
1178 method_setImplementation(mouseUp, origMouseUp);
1179
1180 return true;
1181}
1182
1183- (bool)testModifierKeysAreSynthesizedOnMouseMove:(id)engineMock {
1184 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1185 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1186 [engineMock binaryMessenger])
1187 .andReturn(binaryMessengerMock);
1188
1189 // Need to return a real renderer to allow view controller to load.
1190 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
1191 OCMStub([engineMock renderer]).andReturn(renderer_);
1192
1193 FlutterKeyboardManager* keyboardManager =
1194 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
1195 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
1196
1197 // Capture calls to sendKeyEvent
1198 __block NSMutableArray<KeyEventWrapper*>* events = [NSMutableArray array];
1199 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
1200 callback:nil
1201 userData:nil])
1202 .andDo((^(NSInvocation* invocation) {
1203 FlutterKeyEvent* event;
1204 [invocation getArgument:&event atIndex:2];
1205 [events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
1206 }));
1207
1208 __block NSMutableArray<NSDictionary*>* channelEvents = [NSMutableArray array];
1209 OCMStub([binaryMessengerMock sendOnChannel:@"flutter/keyevent"
1210 message:[OCMArg any]
1211 binaryReply:[OCMArg any]])
1212 .andDo((^(NSInvocation* invocation) {
1213 NSData* data;
1214 [invocation getArgument:&data atIndex:3];
1215 id event = [[FlutterJSONMessageCodec sharedInstance] decode:data];
1216 [channelEvents addObject:event];
1217 }));
1218
1219 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1220 nibName:@""
1221 bundle:nil];
1222 [viewController loadView];
1223 [viewController viewWillAppear];
1224
1225 // Zeroed modifier flag should not synthesize events.
1226 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1227 [viewController mouseMoved:mouseEvent];
1228 EXPECT_EQ([events count], 0u);
1229
1230 // For each modifier key, check that key events are synthesized.
1231 for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
1232 FlutterKeyEvent* event;
1233 NSDictionary* channelEvent;
1234 NSNumber* logicalKey;
1235 NSNumber* physicalKey;
1236 NSEventModifierFlags flag = [flutter::keyCodeToModifierFlag[keyCode] unsignedLongValue];
1237
1238 // Cocoa event always contain combined flags.
1240 flag |= NSEventModifierFlagShift;
1241 }
1243 flag |= NSEventModifierFlagControl;
1244 }
1246 flag |= NSEventModifierFlagOption;
1247 }
1249 flag |= NSEventModifierFlagCommand;
1250 }
1251
1252 // Should synthesize down event.
1253 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(flag);
1254 [viewController mouseMoved:mouseEvent];
1255 EXPECT_EQ([events count], 1u);
1256 event = events[0].data;
1257 logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
1258 physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
1259 EXPECT_EQ(event->type, kFlutterKeyEventTypeDown);
1260 EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
1261 EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
1262 EXPECT_EQ(event->synthesized, true);
1263
1264 channelEvent = channelEvents[0];
1265 EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keydown"]);
1266 EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1267 EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(flag)]);
1268
1269 // Should synthesize up event.
1270 mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1271 [viewController mouseMoved:mouseEvent];
1272 EXPECT_EQ([events count], 2u);
1273 event = events[1].data;
1274 logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
1275 physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
1276 EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
1277 EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
1278 EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
1279 EXPECT_EQ(event->synthesized, true);
1280
1281 channelEvent = channelEvents[1];
1282 EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keyup"]);
1283 EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1284 EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(0)]);
1285
1286 [events removeAllObjects];
1287 [channelEvents removeAllObjects];
1288 };
1289
1290 return true;
1291}
1292
1293- (bool)testViewControllerIsReleased {
1294 __weak FlutterViewController* weakController;
1295 @autoreleasepool {
1296 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1297
1298 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
1299 OCMStub([engineMock renderer]).andReturn(renderer_);
1300
1301 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1302 nibName:@""
1303 bundle:nil];
1304 [viewController loadView];
1305 weakController = viewController;
1306
1307 [engineMock shutDownEngine];
1308 }
1309
1310 EXPECT_EQ(weakController, nil);
1311 return true;
1312}
1313
1314@end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterResult)(id _Nullable result)
GLenum type
@ kPanZoomUpdate
The pan/zoom updated.
Definition embedder.h:1273
@ kPanZoomStart
A pan/zoom started on this pointer.
Definition embedder.h:1271
@ kPanZoomEnd
The pan/zoom ended.
Definition embedder.h:1275
@ kFlutterPointerSignalKindScrollInertiaCancel
Definition embedder.h:1302
@ kFlutterPointerSignalKindScroll
Definition embedder.h:1301
@ kFlutterPointerSignalKindNone
Definition embedder.h:1300
void(* FlutterKeyEventCallback)(bool, void *)
Definition embedder.h:1427
@ kFlutterKeyEventTypeDown
Definition embedder.h:1348
@ kFlutterKeyEventTypeUp
Definition embedder.h:1347
@ kFlutterPointerDeviceKindTrackpad
Definition embedder.h:1283
GLFWwindow * window
Definition main.cc:60
FlutterEngine engine
Definition main.cc:84
FlView * view
return TRUE
const gchar FlBinaryMessengerMessageHandler handler
G_BEGIN_DECLS GBytes * message
FlutterDesktopBinaryReply callback
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterMouseTrackingMode mouseTrackingMode
NSString * lookupKeyForAsset:(NSString *asset)
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
FlutterViewController * viewController
static const FlutterKeyEvent kDefaultFlutterKeyEvent
static const FlutterPointerEvent kDefaultFlutterPointerEvent
static MockEpoxy * mock
Definition mock_epoxy.cc:53
constexpr uint64_t kPhysicalKeyA
Definition key_codes.g.h:77
id CreateMockFlutterEngine(NSString *pasteboardString)
constexpr int64_t kFlutterImplicitViewId
Definition constants.h:35
const NSDictionary * keyCodeToModifierFlag
instancetype sharedInstance()
uint64_t logical
Definition embedder.h:1406
uint64_t physical
Definition embedder.h:1398
FlutterKeyEventType type
The event kind.
Definition embedder.h:1390
std::shared_ptr< const fml::Mapping > data
int BOOL