Flutter Engine
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 "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
7 
8 #import <OCMock/OCMock.h>
9 
10 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
11 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
12 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
13 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
14 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
15 #import "flutter/testing/testing.h"
16 
17 @interface FlutterViewControllerTestObjC : NSObject
23 
24 + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
25  callback:(nullable FlutterKeyEventCallback)callback
26  userData:(nullable void*)userData;
27 @end
28 
29 namespace flutter::testing {
30 
31 namespace {
32 
33 // Allocates and returns an engine configured for the test fixture resource configuration.
34 FlutterEngine* CreateTestEngine() {
35  NSString* fixtures = @(testing::GetFixturesPath());
36  FlutterDartProject* project = [[FlutterDartProject alloc]
37  initWithAssetsPath:fixtures
38  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
39  return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true];
40 }
41 
42 NSResponder* mockResponder() {
43  NSResponder* mock = OCMStrictClassMock([NSResponder class]);
44  OCMStub([mock keyDown:[OCMArg any]]).andDo(nil);
45  OCMStub([mock keyUp:[OCMArg any]]).andDo(nil);
46  OCMStub([mock flagsChanged:[OCMArg any]]).andDo(nil);
47  return mock;
48 }
49 } // namespace
50 
51 TEST(FlutterViewController, HasStringsWhenPasteboardEmpty) {
52  // Mock FlutterViewController so that it behaves like the pasteboard is empty.
53  id viewControllerMock = CreateMockViewController(nil);
54 
55  // Call hasStrings and expect it to be false.
56  __block bool calledAfterClear = false;
57  __block bool valueAfterClear;
58  FlutterResult resultAfterClear = ^(id result) {
59  calledAfterClear = true;
60  NSNumber* valueNumber = [result valueForKey:@"value"];
61  valueAfterClear = [valueNumber boolValue];
62  };
63  FlutterMethodCall* methodCallAfterClear =
64  [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
65  [viewControllerMock handleMethodCall:methodCallAfterClear result:resultAfterClear];
66  EXPECT_TRUE(calledAfterClear);
67  EXPECT_FALSE(valueAfterClear);
68 }
69 
70 TEST(FlutterViewController, HasStringsWhenPasteboardFull) {
71  // Mock FlutterViewController so that it behaves like the pasteboard has a
72  // valid string.
73  id viewControllerMock = CreateMockViewController(@"some string");
74 
75  // Call hasStrings and expect it to be true.
76  __block bool called = false;
77  __block bool value;
78  FlutterResult result = ^(id result) {
79  called = true;
80  NSNumber* valueNumber = [result valueForKey:@"value"];
81  value = [valueNumber boolValue];
82  };
83  FlutterMethodCall* methodCall =
84  [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil];
85  [viewControllerMock handleMethodCall:methodCall result:result];
86  EXPECT_TRUE(called);
87  EXPECT_TRUE(value);
88 }
89 
90 TEST(FlutterViewController, HasViewThatHidesOtherViewsInAccessibility) {
91  FlutterViewController* viewControllerMock = CreateMockViewController(nil);
92 
93  [viewControllerMock loadView];
94  auto subViews = [viewControllerMock.view subviews];
95 
96  EXPECT_EQ([subViews count], 1u);
97  EXPECT_EQ(subViews[0], viewControllerMock.flutterView);
98 
99  NSTextField* textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)];
100  [viewControllerMock.view addSubview:textField];
101 
102  subViews = [viewControllerMock.view subviews];
103  EXPECT_EQ([subViews count], 2u);
104 
105  auto accessibilityChildren = viewControllerMock.view.accessibilityChildren;
106  // The accessibilityChildren should only contains the FlutterView.
107  EXPECT_EQ([accessibilityChildren count], 1u);
108  EXPECT_EQ(accessibilityChildren[0], viewControllerMock.flutterView);
109 }
110 
111 TEST(FlutterViewController, SetsFlutterViewFirstResponderWhenAccessibilityDisabled) {
112  FlutterEngine* engine = CreateTestEngine();
113  NSString* fixtures = @(testing::GetFixturesPath());
114  FlutterDartProject* project = [[FlutterDartProject alloc]
115  initWithAssetsPath:fixtures
116  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
117  FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
118  [viewController loadView];
119  [engine setViewController:viewController];
120  // Creates a NSWindow so that sub view can be first responder.
121  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
122  styleMask:NSBorderlessWindowMask
123  backing:NSBackingStoreBuffered
124  defer:NO];
125  window.contentView = viewController.view;
126  // Attaches FlutterTextInputPlugin to the view;
127  [viewController.view addSubview:viewController.textInputPlugin];
128  // Makes sure the textInputPlugin can be the first responder.
129  EXPECT_TRUE([window makeFirstResponder:viewController.textInputPlugin]);
130  EXPECT_EQ([window firstResponder], viewController.textInputPlugin);
131  // Sends a notification to turn off the accessibility.
132  NSDictionary* userInfo = @{
133  @"AXEnhancedUserInterface" : @(NO),
134  };
135  NSNotification* accessibilityOff = [NSNotification notificationWithName:@""
136  object:nil
137  userInfo:userInfo];
138  [viewController onAccessibilityStatusChanged:accessibilityOff];
139  // FlutterView becomes the first responder.
140  EXPECT_EQ([window firstResponder], viewController.flutterView);
141 }
142 
143 TEST(FlutterViewController, CanSetMouseTrackingModeBeforeViewLoaded) {
144  NSString* fixtures = @(testing::GetFixturesPath());
145  FlutterDartProject* project = [[FlutterDartProject alloc]
146  initWithAssetsPath:fixtures
147  ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
148  FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
149  viewController.mouseTrackingMode = FlutterMouseTrackingModeInActiveApp;
150  ASSERT_EQ(viewController.mouseTrackingMode, FlutterMouseTrackingModeInActiveApp);
151 }
152 
153 TEST(FlutterViewControllerTest, TestKeyEventsAreSentToFramework) {
154  ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsAreSentToFramework]);
155 }
156 
157 TEST(FlutterViewControllerTest, TestKeyEventsArePropagatedIfNotHandled) {
158  ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsArePropagatedIfNotHandled]);
159 }
160 
161 TEST(FlutterViewControllerTest, TestKeyEventsAreNotPropagatedIfHandled) {
162  ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsAreNotPropagatedIfHandled]);
163 }
164 
165 TEST(FlutterViewControllerTest, TestFlagsChangedEventsArePropagatedIfNotHandled) {
166  ASSERT_TRUE(
167  [[FlutterViewControllerTestObjC alloc] testFlagsChangedEventsArePropagatedIfNotHandled]);
168 }
169 
170 TEST(FlutterViewControllerTest, TestPerformKeyEquivalentSynthesizesKeyUp) {
171  ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testPerformKeyEquivalentSynthesizesKeyUp]);
172 }
173 
174 } // namespace flutter::testing
175 
176 @implementation FlutterViewControllerTestObjC
177 
179  id engineMock = OCMClassMock([FlutterEngine class]);
180  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
181  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
182  [engineMock binaryMessenger])
183  .andReturn(binaryMessengerMock);
184  OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
185  callback:nil
186  userData:nil])
187  .andCall([FlutterViewControllerTestObjC class],
188  @selector(respondFalseForSendEvent:callback:userData:));
189  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
190  nibName:@""
191  bundle:nil];
192  NSDictionary* expectedEvent = @{
193  @"keymap" : @"macos",
194  @"type" : @"keydown",
195  @"keyCode" : @(65),
196  @"modifiers" : @(538968064),
197  @"characters" : @".",
198  @"charactersIgnoringModifiers" : @".",
199  };
200  NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
201  CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
202  NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
203  [viewController viewWillAppear]; // Initializes the event channel.
204  [viewController keyDown:event];
205  @try {
206  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
207  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
208  message:encodedKeyEvent
209  binaryReply:[OCMArg any]]);
210  } @catch (...) {
211  return false;
212  }
213  return true;
214 }
215 
217  id engineMock = OCMClassMock([FlutterEngine class]);
218  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
219  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
220  [engineMock binaryMessenger])
221  .andReturn(binaryMessengerMock);
222  OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
223  callback:nil
224  userData:nil])
225  .andCall([FlutterViewControllerTestObjC class],
226  @selector(respondFalseForSendEvent:callback:userData:));
227  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
228  nibName:@""
229  bundle:nil];
230  id responderMock = flutter::testing::mockResponder();
231  viewController.nextResponder = responderMock;
232  NSDictionary* expectedEvent = @{
233  @"keymap" : @"macos",
234  @"type" : @"keydown",
235  @"keyCode" : @(65),
236  @"modifiers" : @(538968064),
237  @"characters" : @".",
238  @"charactersIgnoringModifiers" : @".",
239  };
240  NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
241  CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
242  NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
243  OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
244  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
245  message:encodedKeyEvent
246  binaryReply:[OCMArg any]])
247  .andDo((^(NSInvocation* invocation) {
248  FlutterBinaryReply handler;
249  [invocation getArgument:&handler atIndex:4];
250  NSDictionary* reply = @{
251  @"handled" : @(false),
252  };
253  NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
254  handler(encodedReply);
255  }));
256  [viewController viewWillAppear]; // Initializes the event channel.
257  [viewController keyDown:event];
258  @try {
259  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
260  [responderMock keyDown:[OCMArg any]]);
261  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
262  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
263  message:encodedKeyEvent
264  binaryReply:[OCMArg any]]);
265  } @catch (...) {
266  return false;
267  }
268  return true;
269 }
270 
272  id engineMock = OCMClassMock([FlutterEngine class]);
273  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
274  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
275  [engineMock binaryMessenger])
276  .andReturn(binaryMessengerMock);
277  OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
278  callback:nil
279  userData:nil])
280  .andCall([FlutterViewControllerTestObjC class],
281  @selector(respondFalseForSendEvent:callback:userData:));
282  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
283  nibName:@""
284  bundle:nil];
285  id responderMock = flutter::testing::mockResponder();
286  viewController.nextResponder = responderMock;
287  NSDictionary* expectedEvent = @{
288  @"keymap" : @"macos",
289  @"type" : @"keydown",
290  @"keyCode" : @(56), // SHIFT key
291  @"modifiers" : @(537001986),
292  };
293  NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
294  CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 56, TRUE); // SHIFT key
295  CGEventSetType(cgEvent, kCGEventFlagsChanged);
296  NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
297  OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
298  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
299  message:encodedKeyEvent
300  binaryReply:[OCMArg any]])
301  .andDo((^(NSInvocation* invocation) {
302  FlutterBinaryReply handler;
303  [invocation getArgument:&handler atIndex:4];
304  NSDictionary* reply = @{
305  @"handled" : @(false),
306  };
307  NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
308  handler(encodedReply);
309  }));
310  [viewController viewWillAppear]; // Initializes the event channel.
311  [viewController flagsChanged:event];
312  @try {
313  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
314  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
315  message:encodedKeyEvent
316  binaryReply:[OCMArg any]]);
317  } @catch (NSException* e) {
318  NSLog(@"%@", e.reason);
319  return false;
320  }
321  return true;
322 }
323 
325  id engineMock = OCMClassMock([FlutterEngine class]);
326  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
327  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
328  [engineMock binaryMessenger])
329  .andReturn(binaryMessengerMock);
330  OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
331  callback:nil
332  userData:nil])
333  .andCall([FlutterViewControllerTestObjC class],
334  @selector(respondFalseForSendEvent:callback:userData:));
335  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
336  nibName:@""
337  bundle:nil];
338  id responderMock = flutter::testing::mockResponder();
339  viewController.nextResponder = responderMock;
340  NSDictionary* expectedEvent = @{
341  @"keymap" : @"macos",
342  @"type" : @"keydown",
343  @"keyCode" : @(65),
344  @"modifiers" : @(538968064),
345  @"characters" : @".",
346  @"charactersIgnoringModifiers" : @".",
347  };
348  NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedEvent];
349  CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
350  NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
351  OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
352  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
353  message:encodedKeyEvent
354  binaryReply:[OCMArg any]])
355  .andDo((^(NSInvocation* invocation) {
356  FlutterBinaryReply handler;
357  [invocation getArgument:&handler atIndex:4];
358  NSDictionary* reply = @{
359  @"handled" : @(true),
360  };
361  NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
362  handler(encodedReply);
363  }));
364  [viewController viewWillAppear]; // Initializes the event channel.
365  [viewController keyDown:event];
366  @try {
367  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
368  never(), [responderMock keyDown:[OCMArg any]]);
369  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
370  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
371  message:encodedKeyEvent
372  binaryReply:[OCMArg any]]);
373  } @catch (...) {
374  return false;
375  }
376  return true;
377 }
378 
380  id engineMock = OCMClassMock([FlutterEngine class]);
381  id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
382  OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
383  [engineMock binaryMessenger])
384  .andReturn(binaryMessengerMock);
385  OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
386  callback:nil
387  userData:nil])
388  .andCall([FlutterViewControllerTestObjC class],
389  @selector(respondFalseForSendEvent:callback:userData:));
390  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
391  nibName:@""
392  bundle:nil];
393  id responderMock = flutter::testing::mockResponder();
394  viewController.nextResponder = responderMock;
395  NSDictionary* expectedKeyDownEvent = @{
396  @"keymap" : @"macos",
397  @"type" : @"keydown",
398  @"keyCode" : @(65),
399  @"modifiers" : @(538968064),
400  @"characters" : @".",
401  @"charactersIgnoringModifiers" : @".",
402  };
403  NSData* encodedKeyDownEvent =
404  [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyDownEvent];
405  NSDictionary* expectedKeyUpEvent = @{
406  @"keymap" : @"macos",
407  @"type" : @"keyup",
408  @"keyCode" : @(65),
409  @"modifiers" : @(538968064),
410  @"characters" : @".",
411  @"charactersIgnoringModifiers" : @".",
412  };
413  NSData* encodedKeyUpEvent = [[FlutterJSONMessageCodec sharedInstance] encode:expectedKeyUpEvent];
414  CGEventRef cgEvent = CGEventCreateKeyboardEvent(NULL, 65, TRUE);
415  NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
416  OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
417  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
418  message:encodedKeyDownEvent
419  binaryReply:[OCMArg any]])
420  .andDo((^(NSInvocation* invocation) {
421  FlutterBinaryReply handler;
422  [invocation getArgument:&handler atIndex:4];
423  NSDictionary* reply = @{
424  @"handled" : @(true),
425  };
426  NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
427  handler(encodedReply);
428  }));
429  OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
430  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
431  message:encodedKeyUpEvent
432  binaryReply:[OCMArg any]])
433  .andDo((^(NSInvocation* invocation) {
434  FlutterBinaryReply handler;
435  [invocation getArgument:&handler atIndex:4];
436  NSDictionary* reply = @{
437  @"handled" : @(true),
438  };
439  NSData* encodedReply = [[FlutterJSONMessageCodec sharedInstance] encode:reply];
440  handler(encodedReply);
441  }));
442  [viewController viewWillAppear]; // Initializes the event channel.
443  [viewController performKeyEquivalent:event];
444  @try {
445  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
446  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
447  message:encodedKeyDownEvent
448  binaryReply:[OCMArg any]]);
449  OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
450  [binaryMessengerMock sendOnChannel:@"flutter/keyevent"
451  message:encodedKeyUpEvent
452  binaryReply:[OCMArg any]]);
453  } @catch (...) {
454  return false;
455  }
456  return true;
457 }
458 
459 + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
460  callback:(nullable FlutterKeyEventCallback)callback
461  userData:(nullable void*)userData {
462  if (callback != nullptr)
463  callback(false, userData);
464 }
465 
466 @end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
GAsyncResult * result
id CreateMockViewController(NSString *pasteboardString)
FlKeyEvent FlKeyResponderAsyncCallback callback
instancetype sharedInstance()
uint8_t value
const char * GetFixturesPath()
Returns the directory containing the test fixture for the target if this target has fixtures configur...
void(^ FlutterResult)(id _Nullable result)
instancetype methodCallWithMethodName:arguments:(NSString *method, [arguments] id _Nullable arguments)
void(* FlutterKeyEventCallback)(bool, void *)
Definition: embedder.h:748
TEST(DisplayListCanvas, DrawPaint)