| 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, kCGEventFlagMaskShift | flutter::kModifierFlagShiftLeft);
943 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventIsContinuous, 0);
944 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventDeltaAxis2,
945 0); // scroll_delta_x
946 CGEventSetIntegerValueField(cgEventDiscreteShift, kCGScrollWheelEventDeltaAxis1,
947 2); // scroll_delta_y
948
949 called = false;
950 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventDiscreteShift]];
951 EXPECT_TRUE(called);
952 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindScroll);
953 // pixelsPerLine is 40.0, direction is reversed and axes have been flipped back.
954 EXPECT_FLOAT_EQ(last_event.scroll_delta_x, 0.0 * viewController.flutterView.layer.contentsScale);
955 EXPECT_FLOAT_EQ(last_event.scroll_delta_y,
956 -80.0 * viewController.flutterView.layer.contentsScale);
957
958 // Test for scale events.
959 // Start gesture.
960 called = false;
961 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
962 NSEventPhaseBegan, 1, 0)];
963 EXPECT_TRUE(called);
964 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
965 EXPECT_EQ(last_event.phase, kPanZoomStart);
966 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
967 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
968
969 // Update gesture.
970 called = false;
971 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
972 NSEventPhaseChanged, 1, 0)];
973 EXPECT_TRUE(called);
974 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
975 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
976 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
977 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
978 EXPECT_EQ(last_event.pan_x, 0);
979 EXPECT_EQ(last_event.pan_y, 0);
980 EXPECT_EQ(last_event.scale, 2); // macOS uses logarithmic scaling values, the linear value for
981 // flutter here should be 2^1 = 2.
982 EXPECT_EQ(last_event.rotation, 0);
983
984 // Make sure the scale values accumulate.
985 called = false;
986 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
987 NSEventPhaseChanged, 1, 0)];
988 EXPECT_TRUE(called);
989 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
990 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
991 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
992 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
993 EXPECT_EQ(last_event.pan_x, 0);
994 EXPECT_EQ(last_event.pan_y, 0);
995 EXPECT_EQ(last_event.scale, 4); // macOS uses logarithmic scaling values, the linear value for
996 // flutter here should be 2^(1+1) = 2.
997 EXPECT_EQ(last_event.rotation, 0);
998
999 // End gesture.
1000 called = false;
1001 [viewController magnifyWithEvent:flutter::testing::MockGestureEvent(NSEventTypeMagnify,
1002 NSEventPhaseEnded, 0, 0)];
1003 EXPECT_TRUE(called);
1004 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1005 EXPECT_EQ(last_event.phase, kPanZoomEnd);
1006 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1007 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1008
1009 // Test for rotation events.
1010 // Start gesture.
1011 called = false;
1012 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1013 NSEventPhaseBegan, 1, 0)];
1014 EXPECT_TRUE(called);
1015 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1016 EXPECT_EQ(last_event.phase, kPanZoomStart);
1017 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1018 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1019
1020 // Update gesture.
1021 called = false;
1022 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(
1023 NSEventTypeRotate, NSEventPhaseChanged, 0, -180)]; // degrees
1024 EXPECT_TRUE(called);
1025 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1026 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
1027 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1028 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1029 EXPECT_EQ(last_event.pan_x, 0);
1030 EXPECT_EQ(last_event.pan_y, 0);
1031 EXPECT_EQ(last_event.scale, 1);
1032 EXPECT_EQ(last_event.rotation, M_PI); // radians
1033
1034 // Make sure the rotation values accumulate.
1035 called = false;
1036 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(
1037 NSEventTypeRotate, NSEventPhaseChanged, 0, -360)]; // degrees
1038 EXPECT_TRUE(called);
1039 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1040 EXPECT_EQ(last_event.phase, kPanZoomUpdate);
1041 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1042 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1043 EXPECT_EQ(last_event.pan_x, 0);
1044 EXPECT_EQ(last_event.pan_y, 0);
1045 EXPECT_EQ(last_event.scale, 1);
1046 EXPECT_EQ(last_event.rotation, 3 * M_PI); // radians
1047
1048 // End gesture.
1049 called = false;
1050 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1051 NSEventPhaseEnded, 0, 0)];
1052 EXPECT_TRUE(called);
1053 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1054 EXPECT_EQ(last_event.phase, kPanZoomEnd);
1055 EXPECT_EQ(last_event.device_kind, kFlutterPointerDeviceKindTrackpad);
1056 EXPECT_EQ(last_event.signal_kind, kFlutterPointerSignalKindNone);
1057
1058 // Test that stray NSEventPhaseCancelled event does not crash
1059 called = false;
1060 [viewController rotateWithEvent:flutter::testing::MockGestureEvent(NSEventTypeRotate,
1061 NSEventPhaseCancelled, 0, 0)];
1062 EXPECT_FALSE(called);
1063
1064 return true;
1065 }
1066
1067 // Magic mouse can interleave mouse events with scroll events. This must not crash.
1068 - (bool)mouseAndGestureEventsAreHandledSeparately:(id)engineMock {
1069 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1070 nibName:@""
1071 bundle:nil];
1072 [viewController loadView];
1073
1074 // Test for pan events.
1075 // Start gesture.
1076 fml::CFRef<CGEventRef> cgEventStart(
1077 CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, 0));
1078 CGEventSetType(cgEventStart, kCGEventScrollWheel);
1079 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventScrollPhase, kCGScrollPhaseBegan);
1080 CGEventSetIntegerValueField(cgEventStart, kCGScrollWheelEventIsContinuous, 1);
1081 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventStart]];
1082
1083 fml::CFRef<CGEventRef> cgEventUpdate(CGEventCreateCopy(cgEventStart));
1084 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventScrollPhase, kCGScrollPhaseChanged);
1085 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis2, 1); // pan_x
1086 CGEventSetIntegerValueField(cgEventUpdate, kCGScrollWheelEventDeltaAxis1, 2); // pan_y
1087 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventUpdate]];
1088
1089 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1090 [viewController mouseEntered:mouseEvent];
1091 [viewController mouseExited:mouseEvent];
1092
1093 // End gesture.
1094 fml::CFRef<CGEventRef> cgEventEnd(CGEventCreateCopy(cgEventStart));
1095 CGEventSetIntegerValueField(cgEventEnd, kCGScrollWheelEventScrollPhase, kCGScrollPhaseEnded);
1096 [viewController scrollWheel:[NSEvent eventWithCGEvent:cgEventEnd]];
1097
1098 return true;
1099 }
1100
1101 - (bool)testViewWillAppearCalledMultipleTimes:(id)engineMock {
1102 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1103 nibName:@""
1104 bundle:nil];
1105 [viewController viewWillAppear];
1106 [viewController viewWillAppear];
1107 return true;
1108 }
1109
1110 - (bool)testLookupKeyAssets {
1111 FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:nil];
1112 NSString* key = [viewController lookupKeyForAsset:@"test.png"];
1113 EXPECT_TRUE(
1114 [key isEqualToString:@"Contents/Frameworks/App.framework/Resources/flutter_assets/test.png"]);
1115 return true;
1116 }
1117
1118 - (bool)testLookupKeyAssetsWithPackage {
1119 FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:nil];
1120
1121 NSString* packageKey = [viewController lookupKeyForAsset:@"test.png" fromPackage:@"test"];
1122 EXPECT_TRUE([packageKey
1123 isEqualToString:
1124 @"Contents/Frameworks/App.framework/Resources/flutter_assets/packages/test/test.png"]);
1125 return true;
1126 }
1127
1128 static void SwizzledNoop(id self, SEL _cmd) {}
1129
1130 // Verify workaround an AppKit bug where mouseDown/mouseUp are not called on the view controller if
1131 // the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
1132 // setting is enabled.
1133 //
1134 // See: https://github.com/flutter/flutter/issues/115015
1135 // See: http://www.openradar.me/FB12050037
1136 // See: https://developer.apple.com/documentation/appkit/nsresponder/1524634-mousedown
1137 //
1138 // TODO(cbracken): https://github.com/flutter/flutter/issues/154063
1139 // Remove this test when we drop support for macOS 12 (Monterey).
1140 - (bool)testMouseDownUpEventsSentToNextResponder:(id)engineMock {
1141 if (@available(macOS 13.3.1, *)) {
1142 // This workaround is disabled for macOS 13.3.1 onwards, since the underlying AppKit bug is
1143 // fixed.
1144 return true;
1145 }
1146
1147 // The root cause of the above bug is NSResponder mouseDown/mouseUp methods that don't correctly
1148 // walk the responder chain calling the appropriate method on the next responder under certain
1149 // conditions. Simulate this by swizzling out the default implementations and replacing them with
1150 // no-ops.
1151 Method mouseDown = class_getInstanceMethod([NSResponder class], @selector(mouseDown:));
1152 Method mouseUp = class_getInstanceMethod([NSResponder class], @selector(mouseUp:));
1153 IMP noopImp = (IMP)SwizzledNoop;
1154 IMP origMouseDown = method_setImplementation(mouseDown, noopImp);
1155 IMP origMouseUp = method_setImplementation(mouseUp, noopImp);
1156
1157 // Verify that mouseDown/mouseUp trigger mouseDown/mouseUp calls on FlutterViewController.
1158 MouseEventFlutterViewController* viewController =
1159 [[MouseEventFlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil];
1160 FlutterView* view = (FlutterView*)[viewController view];
1161
1162 EXPECT_FALSE(viewController.mouseDownCalled);
1163 EXPECT_FALSE(viewController.mouseUpCalled);
1164
1165 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1166 [view mouseDown:mouseEvent];
1167 EXPECT_TRUE(viewController.mouseDownCalled);
1168 EXPECT_FALSE(viewController.mouseUpCalled);
1169
1170 viewController.mouseDownCalled = NO;
1171 [view mouseUp:mouseEvent];
1172 EXPECT_FALSE(viewController.mouseDownCalled);
1173 EXPECT_TRUE(viewController.mouseUpCalled);
1174
1175 // Restore the original NSResponder mouseDown/mouseUp implementations.
1176 method_setImplementation(mouseDown, origMouseDown);
1177 method_setImplementation(mouseUp, origMouseUp);
1178
1179 return true;
1180 }
1181
1182 - (bool)testModifierKeysAreSynthesizedOnMouseMove:(id)engineMock {
1183 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1184 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1185 [engineMock binaryMessenger])
1186 .andReturn(binaryMessengerMock);
1187
1188 // Need to return a real renderer to allow view controller to load.
1189 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
1190 OCMStub([engineMock renderer]).andReturn(renderer_);
1191
1192 FlutterKeyboardManager* keyboardManager =
1193 [[FlutterKeyboardManager alloc] initWithDelegate:engineMock];
1194 OCMStub([engineMock keyboardManager]).andReturn(keyboardManager);
1195
1196 // Capture calls to sendKeyEvent
1197 __block NSMutableArray<KeyEventWrapper*>* events = [NSMutableArray array];
1198 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:kDefaultFlutterKeyEvent
1199 callback:nil
1200 userData:nil])
1201 .andDo((^(NSInvocation* invocation) {
1202 FlutterKeyEvent* event;
1203 [invocation getArgument:&event atIndex:2];
1204 [events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
1205 }));
1206
1207 __block NSMutableArray<NSDictionary*>* channelEvents = [NSMutableArray array];
1208 OCMStub([binaryMessengerMock sendOnChannel:@"flutter/keyevent"
1209 message:[OCMArg any]
1210 binaryReply:[OCMArg any]])
1211 .andDo((^(NSInvocation* invocation) {
1212 NSData* data;
1213 [invocation getArgument:&data atIndex:3];
1214 id event = [[FlutterJSONMessageCodec sharedInstance] decode:data];
1215 [channelEvents addObject:event];
1216 }));
1217
1218 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1219 nibName:@""
1220 bundle:nil];
1221 [viewController loadView];
1222 [viewController viewWillAppear];
1223
1224 // Zeroed modifier flag should not synthesize events.
1225 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1226 [viewController mouseMoved:mouseEvent];
1227 EXPECT_EQ([events count], 0u);
1228
1229 // For each modifier key, check that key events are synthesized.
1230 for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
1231 FlutterKeyEvent* event;
1232 NSDictionary* channelEvent;
1233 NSNumber* logicalKey;
1234 NSNumber* physicalKey;
1235 NSEventModifierFlags flag = [flutter::keyCodeToModifierFlag[keyCode] unsignedLongValue];
1236
1237 // Cocoa event always contain combined flags.
1238 if (flag & (flutter::kModifierFlagShiftLeft | flutter::kModifierFlagShiftRight)) {
1239 flag |= NSEventModifierFlagShift;
1240 }
1241 if (flag & (flutter::kModifierFlagControlLeft | flutter::kModifierFlagControlRight)) {
1242 flag |= NSEventModifierFlagControl;
1243 }
1244 if (flag & (flutter::kModifierFlagAltLeft | flutter::kModifierFlagAltRight)) {
1245 flag |= NSEventModifierFlagOption;
1246 }
1247 if (flag & (flutter::kModifierFlagMetaLeft | flutter::kModifierFlagMetaRight)) {
1248 flag |= NSEventModifierFlagCommand;
1249 }
1250
1251 // Should synthesize down event.
1252 NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(flag);
1253 [viewController mouseMoved:mouseEvent];
1254 EXPECT_EQ([events count], 1u);
1255 event = events[0].data;
1256 logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
1257 physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
1258 EXPECT_EQ(event->type, kFlutterKeyEventTypeDown);
1259 EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
1260 EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
1261 EXPECT_EQ(event->synthesized, true);
1262
1263 channelEvent = channelEvents[0];
1264 EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keydown"]);
1265 EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1266 EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(flag)]);
1267
1268 // Should synthesize up event.
1269 mouseEvent = flutter::testing::CreateMouseEvent(0x00);
1270 [viewController mouseMoved:mouseEvent];
1271 EXPECT_EQ([events count], 2u);
1272 event = events[1].data;
1273 logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
1274 physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
1275 EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
1276 EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
1277 EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
1278 EXPECT_EQ(event->synthesized, true);
1279
1280 channelEvent = channelEvents[1];
1281 EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keyup"]);
1282 EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1283 EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(0)]);
1284
1285 [events removeAllObjects];
1286 [channelEvents removeAllObjects];
1287 };
1288
1289 return true;
1290 }
1291
1292 - (bool)testViewControllerIsReleased {
1293 __weak FlutterViewController* weakController;
1294 @autoreleasepool {
1295 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1296
1297 FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
1298 OCMStub([engineMock renderer]).andReturn(renderer_);
1299
1300 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1301 nibName:@""
1302 bundle:nil];
1303 [viewController loadView];
1304 weakController = viewController;
1305
1306 [engineMock shutDownEngine];
1307 }
1308
1309 EXPECT_EQ(weakController, nil);
1310 return true;
1311 }
1312
1313 @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:24
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:1118
-[FlutterViewControllerTestObjC testViewControllerIsReleased]
bool testViewControllerIsReleased()
Definition: FlutterViewControllerTest.mm:1292
-[FlutterViewControllerTestObjC testLookupKeyAssets]
bool testLookupKeyAssets()
Definition: FlutterViewControllerTest.mm:1110
FlutterView
Definition: FlutterView.h:36
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()