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