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