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