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