| Flutter macOS Embedder |

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
5 #import "KeyCodeMap_Internal.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
7 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
8
9 #import <OCMock/OCMock.h>
10
11 #include "flutter/common/constants.h"
12 #include "flutter/fml/platform/darwin/cf_utils.h"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
14 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
15 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
16 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
17 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
18 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
19 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
20 #include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
21 #include "flutter/testing/autoreleasepool_test.h"
22 #include "flutter/testing/testing.h"
23
24 #pragma mark - Test Helper Classes
25
26 static const FlutterPointerEvent kDefaultFlutterPointerEvent = {};
27 static const FlutterKeyEvent kDefaultFlutterKeyEvent = {};
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.
85 @interface MouseEventFlutterViewController : FlutterViewController
86 @property(nonatomic, assign) BOOL mouseDownCalled;
87 @property(nonatomic, assign) BOOL mouseUpCalled;
88 @end
89
90 @implementation MouseEventFlutterViewController
91 - (void)mouseDown:(NSEvent*)event {
92 self.mouseDownCalled = YES;
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
125 using namespace ::flutter::testing::keycodes;
126
127 namespace flutter::testing {
128
129 namespace {
130
131 id 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.
151 FlutterEngine* 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
159 NSResponder* 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
167 NSEvent* 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.
184 using FlutterViewControllerTest = AutoreleasePoolTest;
185 using FlutterViewControllerMockEngineTest = MockFlutterEngineTest;
186
187 TEST_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
208 TEST_F(FlutterViewControllerTest, FlutterViewAcceptsFirstMouse) {
209 FlutterViewController* viewControllerMock = CreateMockViewController();
210 [viewControllerMock loadView];
211 EXPECT_EQ([viewControllerMock.flutterView acceptsFirstMouse:nil], YES);
212 }
213
214 TEST_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
251 TEST_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
261 TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsAreSentToFramework) {
262 id mockEngine = GetMockEngine();
263 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsAreSentToFramework:mockEngine]);
264 }
265
266 TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsArePropagatedIfNotHandled) {
267 id mockEngine = GetMockEngine();
268 ASSERT_TRUE(
269 [[FlutterViewControllerTestObjC alloc] testKeyEventsArePropagatedIfNotHandled:mockEngine]);
270 }
271
272 TEST_F(FlutterViewControllerMockEngineTest, TestKeyEventsAreNotPropagatedIfHandled) {
273 id mockEngine = GetMockEngine();
274 ASSERT_TRUE(
275 [[FlutterViewControllerTestObjC alloc] testKeyEventsAreNotPropagatedIfHandled:mockEngine]);
276 }
277
278 TEST_F(FlutterViewControllerMockEngineTest, TestCtrlTabKeyEventIsPropagated) {
279 id mockEngine = GetMockEngine();
280 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testCtrlTabKeyEventIsPropagated:mockEngine]);
281 }
282
283 TEST_F(FlutterViewControllerMockEngineTest, TestKeyEquivalentIsPassedToTextInputPlugin) {
284 id mockEngine = GetMockEngine();
285 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc]
286 testKeyEquivalentIsPassedToTextInputPlugin:mockEngine]);
287 }
288
289 TEST_F(FlutterViewControllerMockEngineTest, TestFlagsChangedEventsArePropagatedIfNotHandled) {
290 id mockEngine = GetMockEngine();
291 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc]
292 testFlagsChangedEventsArePropagatedIfNotHandled:mockEngine]);
293 }
294
295 TEST_F(FlutterViewControllerMockEngineTest, TestKeyboardIsRestartedOnEngineRestart) {
296 id mockEngine = GetMockEngine();
297 ASSERT_TRUE(
298 [[FlutterViewControllerTestObjC alloc] testKeyboardIsRestartedOnEngineRestart:mockEngine]);
299 }
300
301 TEST_F(FlutterViewControllerMockEngineTest, TestTrackpadGesturesAreSentToFramework) {
302 id mockEngine = GetMockEngine();
303 ASSERT_TRUE(
304 [[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework:mockEngine]);
305 }
306
307 TEST_F(FlutterViewControllerMockEngineTest, TestmouseAndGestureEventsAreHandledSeparately) {
308 id mockEngine = GetMockEngine();
309 ASSERT_TRUE(
310 [[FlutterViewControllerTestObjC alloc] mouseAndGestureEventsAreHandledSeparately:mockEngine]);
311 }
312
313 TEST_F(FlutterViewControllerMockEngineTest, TestMouseDownUpEventsSentToNextResponder) {
314 id mockEngine = GetMockEngine();
315 ASSERT_TRUE(
316 [[FlutterViewControllerTestObjC alloc] testMouseDownUpEventsSentToNextResponder:mockEngine]);
317 }
318
319 TEST_F(FlutterViewControllerMockEngineTest, TestModifierKeysAreSynthesizedOnMouseMove) {
320 id mockEngine = GetMockEngine();
321 ASSERT_TRUE(
322 [[FlutterViewControllerTestObjC alloc] testModifierKeysAreSynthesizedOnMouseMove:mockEngine]);
323 }
324
325 TEST_F(FlutterViewControllerMockEngineTest, testViewWillAppearCalledMultipleTimes) {
326 id mockEngine = GetMockEngine();
327 ASSERT_TRUE(
328 [[FlutterViewControllerTestObjC alloc] testViewWillAppearCalledMultipleTimes:mockEngine]);
329 }
330
331 TEST_F(FlutterViewControllerMockEngineTest, testFlutterViewIsConfigured) {
332 id mockEngine = GetMockEngine();
333 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testFlutterViewIsConfigured:mockEngine]);
334 }
335
336 TEST_F(FlutterViewControllerTest, testLookupKeyAssets) {
337 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testLookupKeyAssets]);
338 }
339
340 TEST_F(FlutterViewControllerTest, testLookupKeyAssetsWithPackage) {
341 ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testLookupKeyAssetsWithPackage]);
342 }
343
344 TEST_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) {
542 FlutterBinaryReply handler;
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) {
618 FlutterBinaryReply handler;
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) {
675 FlutterBinaryReply handler;
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
1129 static 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.
1159 MouseEventFlutterViewController* viewController =
1160 [[MouseEventFlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil];
1161 FlutterView* view = (FlutterView*)[viewController view];
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.
1239 if (flag & (flutter::kModifierFlagShiftLeft | flutter::kModifierFlagShiftRight)) {
1240 flag |= NSEventModifierFlagShift;
1241 }
1242 if (flag & (flutter::kModifierFlagControlLeft | flutter::kModifierFlagControlRight)) {
1243 flag |= NSEventModifierFlagControl;
1244 }
1245 if (flag & (flutter::kModifierFlagAltLeft | flutter::kModifierFlagAltRight)) {
1246 flag |= NSEventModifierFlagOption;
1247 }
1248 if (flag & (flutter::kModifierFlagMetaLeft | flutter::kModifierFlagMetaRight)) {
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
FlutterBinaryMessenger.h
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterDartProject_Internal.h
FlutterEngine.h
FlutterEngine_Internal.h
FlutterEngineTestUtils.h
FlutterRenderer.h
FlutterViewController.h
FlutterViewController_Internal.h
kDefaultFlutterKeyEvent
static const FlutterKeyEvent kDefaultFlutterKeyEvent
Definition: FlutterViewControllerTest.mm:27
kDefaultFlutterPointerEvent
static const FlutterPointerEvent kDefaultFlutterPointerEvent
Definition: FlutterViewControllerTest.mm:26
FlutterViewControllerTestUtils.h
KeyCodeMap_Internal.h
FlutterDartProject
Definition: FlutterDartProject.mm:39
flutter::testing::MockFlutterEngineTest
Definition: FlutterEngineTestUtils.h:48
FlutterEngine
Definition: FlutterEngine.h:32
FlutterJSONMessageCodec
Definition: FlutterCodecs.h:81
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:79
FlutterMethodCall
Definition: FlutterCodecs.h:221
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterRenderer
Definition: FlutterRenderer.h:19
FlutterResponderWrapper
Definition: FlutterViewControllerTest.mm:51
FlutterResponderWrapper::_responder
NSResponder * _responder
Definition: FlutterViewControllerTest.mm:52
FlutterViewController
Definition: FlutterViewController.h:73
-[FlutterViewController lookupKeyForAsset:]
nonnull NSString * lookupKeyForAsset:(nonnull NSString *asset)
-[FlutterViewController lookupKeyForAsset:fromPackage:]
nonnull NSString * lookupKeyForAsset:fromPackage:(nonnull NSString *asset,[fromPackage] nonnull NSString *package)
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterViewController::mouseTrackingMode
FlutterMouseTrackingMode mouseTrackingMode
Definition: FlutterViewController.h:84
-[FlutterViewController onAccessibilityStatusChanged:]
void onAccessibilityStatusChanged:(BOOL enabled)
FlutterViewControllerTestObjC
Definition: FlutterViewControllerTest.mm:101
-[FlutterViewControllerTestObjC testLookupKeyAssetsWithPackage]
bool testLookupKeyAssetsWithPackage()
Definition: FlutterViewControllerTest.mm:1119
-[FlutterViewControllerTestObjC testViewControllerIsReleased]
bool testViewControllerIsReleased()
Definition: FlutterViewControllerTest.mm:1293
-[FlutterViewControllerTestObjC testLookupKeyAssets]
bool testLookupKeyAssets()
Definition: FlutterViewControllerTest.mm:1111
FlutterView
Definition: FlutterView.h:69
KeyEventWrapper
Definition: FlutterViewControllerTest.mm:31
KeyEventWrapper::data
FlutterKeyEvent * data
Definition: FlutterViewControllerTest.mm:31
MouseEventFlutterViewController
Definition: FlutterViewControllerTest.mm:86
MouseEventFlutterViewController::mouseUpCalled
BOOL mouseUpCalled
Definition: FlutterViewControllerTest.mm:87
MouseEventFlutterViewController::mouseDownCalled
BOOL mouseDownCalled
Definition: FlutterViewControllerTest.mm:86
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
flutter::testing::CreateMockViewController
id CreateMockViewController()
Definition: FlutterViewControllerTestUtils.mm:9
flutter::testing::CreateMockFlutterEngine
id CreateMockFlutterEngine(NSString *pasteboardString)
Definition: FlutterEngineTestUtils.mm:76
flutter::testing::FlutterViewControllerTest
AutoreleasePoolTest FlutterViewControllerTest
Definition: FlutterViewControllerTest.mm:184
flutter::testing::TEST_F
TEST_F(FlutterViewControllerTest, testViewControllerIsReleased)
Definition: FlutterViewControllerTest.mm:344
flutter::keyCodeToModifierFlag
const NSDictionary * keyCodeToModifierFlag
Definition: KeyCodeMap.g.mm:223
flutter::kModifierFlagShiftLeft
@ kModifierFlagShiftLeft
Definition: KeyCodeMap_Internal.h:81
flutter::kModifierFlagAltLeft
@ kModifierFlagAltLeft
Definition: KeyCodeMap_Internal.h:85
flutter::kModifierFlagMetaRight
@ kModifierFlagMetaRight
Definition: KeyCodeMap_Internal.h:84
flutter::kModifierFlagMetaLeft
@ kModifierFlagMetaLeft
Definition: KeyCodeMap_Internal.h:83
flutter::kModifierFlagControlLeft
@ kModifierFlagControlLeft
Definition: KeyCodeMap_Internal.h:80
flutter::kModifierFlagAltRight
@ kModifierFlagAltRight
Definition: KeyCodeMap_Internal.h:86
flutter::kModifierFlagControlRight
@ kModifierFlagControlRight
Definition: KeyCodeMap_Internal.h:87
flutter::kModifierFlagShiftRight
@ kModifierFlagShiftRight
Definition: KeyCodeMap_Internal.h:82
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()