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