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"
8 
9 #import <OCMock/OCMock.h>
10 
11 #include "flutter/common/constants.h"
12 #include "flutter/fml/platform/darwin/cf_utils.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.
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;
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;
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 
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.
1239  flag |= NSEventModifierFlagShift;
1240  }
1242  flag |= NSEventModifierFlagControl;
1243  }
1245  flag |= NSEventModifierFlagOption;
1246  }
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 
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
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterResult)(id _Nullable result)
static const FlutterKeyEvent kDefaultFlutterKeyEvent
static const FlutterPointerEvent kDefaultFlutterPointerEvent
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
nonnull NSString * lookupKeyForAsset:(nonnull NSString *asset)
nonnull NSString * lookupKeyForAsset:fromPackage:(nonnull NSString *asset,[fromPackage] nonnull NSString *package)
FlutterMouseTrackingMode mouseTrackingMode
void onAccessibilityStatusChanged:(BOOL enabled)
id CreateMockFlutterEngine(NSString *pasteboardString)
AutoreleasePoolTest FlutterViewControllerTest
TEST_F(FlutterViewControllerTest, testViewControllerIsReleased)
const NSDictionary * keyCodeToModifierFlag
@ kModifierFlagControlLeft
@ kModifierFlagControlRight
instancetype sharedInstance()