| 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"
6 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
7 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
8
9 #import <OCMock/OCMock.h>
10
11 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
12 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
13 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
14 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
15 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
16 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
17 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
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.
83 @interface MouseEventFlutterViewController : FlutterViewController
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;
113 - (bool)testLookupKeyAssetsWithPackage;
114 - (bool)testViewControllerIsReleased;
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;
183 using FlutterViewControllerMockEngineTest = MockFlutterEngineTest;
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
1061 - (bool)testLookupKeyAssets {
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
1069 - (bool)testLookupKeyAssetsWithPackage {
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.
1185 if (flag & (flutter::kModifierFlagShiftLeft | flutter::kModifierFlagShiftRight)) {
1186 flag |= NSEventModifierFlagShift;
1187 }
1188 if (flag & (flutter::kModifierFlagControlLeft | flutter::kModifierFlagControlRight)) {
1189 flag |= NSEventModifierFlagControl;
1190 }
1191 if (flag & (flutter::kModifierFlagAltLeft | flutter::kModifierFlagAltRight)) {
1192 flag |= NSEventModifierFlagOption;
1193 }
1194 if (flag & (flutter::kModifierFlagMetaLeft | flutter::kModifierFlagMetaRight)) {
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
1239 - (bool)testViewControllerIsReleased {
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