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