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