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