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