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