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 <OCMock/OCMock.h>
6 #import <XCTest/XCTest.h>
7 
8 #include "flutter/fml/platform/darwin/message_loop_darwin.h"
9 #import "flutter/lib/ui/window/viewport_metrics.h"
10 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
11 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
12 #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
13 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
14 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
16 #import "flutter/shell/platform/embedder/embedder.h"
17 
19 
20 @interface FlutterEngine ()
22 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
23  callback:(nullable FlutterKeyEventCallback)callback
24  userData:(nullable void*)userData;
25 @end
26 
27 namespace flutter {
28 class PointerDataPacket {};
29 }
30 
31 /// Sometimes we have to use a custom mock to avoid retain cycles in OCMock.
32 /// Used for testing low memory notification.
34 @property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel;
35 @property(nonatomic, strong) FlutterBasicMessageChannel* keyEventChannel;
36 @property(nonatomic, weak) FlutterViewController* viewController;
37 @property(nonatomic, strong) FlutterTextInputPlugin* textInputPlugin;
38 @property(nonatomic, assign) BOOL didCallNotifyLowMemory;
40 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
41  callback:(nullable FlutterKeyEventCallback)callback
42  userData:(nullable void*)userData;
43 @end
44 
45 @implementation FlutterEnginePartialMock
46 @synthesize viewController;
47 @synthesize lifecycleChannel;
48 @synthesize keyEventChannel;
49 @synthesize textInputPlugin;
50 
51 - (void)notifyLowMemory {
52  _didCallNotifyLowMemory = YES;
53 }
54 
55 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
57  userData:(void*)userData API_AVAILABLE(ios(9.0)) {
58  if (callback == nil)
59  return;
60  // NSAssert(callback != nullptr, @"Invalid callback");
61  // Response is async, so we have to post it to the run loop instead of calling
62  // it directly.
63  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
64  ^() {
65  callback(true, userData);
66  });
67 }
68 @end
69 
70 @interface FlutterEngine ()
71 - (BOOL)createShell:(NSString*)entrypoint
72  libraryURI:(NSString*)libraryURI
73  initialRoute:(NSString*)initialRoute;
74 - (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet;
75 - (void)updateViewportMetrics:(flutter::ViewportMetrics)viewportMetrics;
76 - (void)attachView;
77 @end
78 
80 - (void)notifyLowMemory;
81 @end
82 
83 extern NSNotificationName const FlutterViewControllerWillDealloc;
84 
85 /// A simple mock class for FlutterEngine.
86 ///
87 /// OCMClassMock can't be used for FlutterEngine sometimes because OCMock retains arguments to
88 /// invocations and since the init for FlutterViewController calls a method on the
89 /// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to
90 /// deleting FlutterViewControllers.
91 ///
92 /// Used for testing deallocation.
93 @interface MockEngine : NSObject
94 @end
95 
96 @implementation MockEngine
97 - (FlutterViewController*)viewController {
98  return nil;
99 }
100 - (void)setViewController:(FlutterViewController*)viewController {
101  // noop
102 }
103 @end
104 
105 // The following conditional compilation defines an API 13 concept on earlier API targets so that
106 // a compiler compiling against API 12 or below does not blow up due to non-existent members.
107 #if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
108 typedef enum UIAccessibilityContrast : NSInteger {
113 
116 @end
117 #endif
118 
120 - (void)surfaceUpdated:(BOOL)appeared;
121 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
122 - (void)handlePressEvent:(FlutterUIPressProxy*)press
123  nextAction:(void (^)())next API_AVAILABLE(ios(13.4));
124 - (void)scrollEvent:(UIPanGestureRecognizer*)recognizer;
125 - (void)updateViewportMetrics;
126 - (void)onUserSettingsChanged:(NSNotification*)notification;
127 @end
128 
129 @interface FlutterViewControllerTest : XCTestCase
130 @property(nonatomic, strong) id mockEngine;
131 @property(nonatomic, strong) id mockTextInputPlugin;
132 @property(nonatomic, strong) id messageSent;
133 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;
134 @end
135 
136 @implementation FlutterViewControllerTest
137 
138 - (void)setUp {
139  self.mockEngine = OCMClassMock([FlutterEngine class]);
140  self.mockTextInputPlugin = OCMClassMock([FlutterTextInputPlugin class]);
141  OCMStub([self.mockEngine textInputPlugin]).andReturn(self.mockTextInputPlugin);
142  self.messageSent = nil;
143 }
144 
145 - (void)tearDown {
146  // We stop mocking here to avoid retain cycles that stop
147  // FlutterViewControllers from deallocing.
148  [self.mockEngine stopMocking];
149  self.mockEngine = nil;
150  self.mockTextInputPlugin = nil;
151  self.messageSent = nil;
152 }
153 
154 - (void)testViewDidDisappearDoesntPauseEngineWhenNotTheViewController {
155  id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
157  mockEngine.lifecycleChannel = lifecycleChannel;
158  FlutterViewController* viewControllerA =
159  [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
160  FlutterViewController* viewControllerB =
161  [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
162  id viewControllerMock = OCMPartialMock(viewControllerA);
163  OCMStub([viewControllerMock surfaceUpdated:NO]);
164  mockEngine.viewController = viewControllerB;
165  [viewControllerA viewDidDisappear:NO];
166  OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
167  OCMReject([viewControllerMock surfaceUpdated:[OCMArg any]]);
168 }
169 
170 - (void)testViewDidDisappearDoesPauseEngineWhenIsTheViewController {
171  id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
173  mockEngine.lifecycleChannel = lifecycleChannel;
174  __weak FlutterViewController* weakViewController;
175  @autoreleasepool {
176  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
177  nibName:nil
178  bundle:nil];
179  weakViewController = viewController;
180  id viewControllerMock = OCMPartialMock(viewController);
181  OCMStub([viewControllerMock surfaceUpdated:NO]);
182  [viewController viewDidDisappear:NO];
183  OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
184  OCMVerify([viewControllerMock surfaceUpdated:NO]);
185  }
186  XCTAssertNil(weakViewController);
187 }
188 
189 - (void)
190  testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillAppear {
191  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
192  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
193  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
194  nibName:nil
195  bundle:nil];
196  [viewController viewWillAppear:YES];
197  OCMVerify([viewController onUserSettingsChanged:nil]);
198 }
199 
200 - (void)
201  testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillAppear {
202  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
203  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
204  FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
205  nibName:nil
206  bundle:nil];
207  mockEngine.viewController = nil;
208  FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
209  nibName:nil
210  bundle:nil];
211  mockEngine.viewController = nil;
212  mockEngine.viewController = viewControllerB;
213  [viewControllerA viewWillAppear:YES];
214  OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
215 }
216 
217 - (void)
218  testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewDidAppear {
219  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
220  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
221  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
222  nibName:nil
223  bundle:nil];
224  [viewController viewDidAppear:YES];
225  OCMVerify([viewController onUserSettingsChanged:nil]);
226 }
227 
228 - (void)
229  testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewDidAppear {
230  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
231  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
232  FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
233  nibName:nil
234  bundle:nil];
235  mockEngine.viewController = nil;
236  FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
237  nibName:nil
238  bundle:nil];
239  mockEngine.viewController = nil;
240  mockEngine.viewController = viewControllerB;
241  [viewControllerA viewDidAppear:YES];
242  OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
243 }
244 
245 - (void)
246  testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillDisappear {
247  id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
249  mockEngine.lifecycleChannel = lifecycleChannel;
250  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
251  nibName:nil
252  bundle:nil];
253  mockEngine.viewController = viewController;
254  [viewController viewWillDisappear:NO];
255  OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
256 }
257 
258 - (void)
259  testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillDisappear {
260  id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
262  mockEngine.lifecycleChannel = lifecycleChannel;
263  FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
264  nibName:nil
265  bundle:nil];
266  FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
267  nibName:nil
268  bundle:nil];
269  mockEngine.viewController = viewControllerB;
270  [viewControllerA viewDidDisappear:NO];
271  OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
272 }
273 
274 - (void)testUpdateViewportMetricsDoesntInvokeEngineWhenNotTheViewController {
275  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
276  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
277  FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
278  nibName:nil
279  bundle:nil];
280  mockEngine.viewController = nil;
281  FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
282  nibName:nil
283  bundle:nil];
284  mockEngine.viewController = viewControllerB;
285  [viewControllerA updateViewportMetrics];
286  flutter::ViewportMetrics viewportMetrics;
287  OCMVerify(never(), [mockEngine updateViewportMetrics:viewportMetrics]);
288 }
289 
290 - (void)testUpdateViewportMetricsDoesInvokeEngineWhenIsTheViewController {
291  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
292  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
293  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
294  nibName:nil
295  bundle:nil];
296  mockEngine.viewController = viewController;
297  [viewController updateViewportMetrics];
298  flutter::ViewportMetrics viewportMetrics;
299  OCMVerify([mockEngine updateViewportMetrics:viewportMetrics]);
300 }
301 
302 - (void)testViewDidLoadDoesntInvokeEngineWhenNotTheViewController {
303  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
304  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
305  FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
306  nibName:nil
307  bundle:nil];
308  mockEngine.viewController = nil;
309  FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
310  nibName:nil
311  bundle:nil];
312  mockEngine.viewController = viewControllerB;
313  UIView* view = viewControllerA.view;
314  XCTAssertNotNil(view);
315  OCMVerify(never(), [mockEngine attachView]);
316 }
317 
318 - (void)testViewDidLoadDoesInvokeEngineWhenIsTheViewController {
319  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
320  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
321  mockEngine.viewController = nil;
322  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
323  nibName:nil
324  bundle:nil];
325  mockEngine.viewController = viewController;
326  UIView* view = viewController.view;
327  XCTAssertNotNil(view);
328  OCMVerify([mockEngine attachView]);
329 }
330 
331 - (void)testBinaryMessenger {
332  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
333  nibName:nil
334  bundle:nil];
335  XCTAssertNotNil(vc);
336  id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
337  OCMStub([self.mockEngine binaryMessenger]).andReturn(messenger);
338  XCTAssertEqual(vc.binaryMessenger, messenger);
339  OCMVerify([self.mockEngine binaryMessenger]);
340 }
341 
342 #pragma mark - Platform Brightness
343 
344 - (void)testItReportsLightPlatformBrightnessByDefault {
345  // Setup test.
346  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
347  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
348 
349  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
350  nibName:nil
351  bundle:nil];
352 
353  // Exercise behavior under test.
354  [vc traitCollectionDidChange:nil];
355 
356  // Verify behavior.
357  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
358  return [message[@"platformBrightness"] isEqualToString:@"light"];
359  }]]);
360 
361  // Clean up mocks
362  [settingsChannel stopMocking];
363 }
364 
365 - (void)testItReportsPlatformBrightnessWhenViewWillAppear {
366  // Setup test.
367  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
368  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
369  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
370  OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
371  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
372  nibName:nil
373  bundle:nil];
374 
375  // Exercise behavior under test.
376  [vc viewWillAppear:false];
377 
378  // Verify behavior.
379  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
380  return [message[@"platformBrightness"] isEqualToString:@"light"];
381  }]]);
382 
383  // Clean up mocks
384  [settingsChannel stopMocking];
385 }
386 
387 - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
388  if (@available(iOS 13, *)) {
389  // noop
390  } else {
391  return;
392  }
393 
394  // Setup test.
395  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
396  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
397 
398  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
399  nibName:nil
400  bundle:nil];
401  id mockTraitCollection =
402  [self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
403 
404  // We partially mock the real FlutterViewController to act as the OS and report
405  // the UITraitCollection of our choice. Mocking the object under test is not
406  // desirable, but given that the OS does not offer a DI approach to providing
407  // our own UITraitCollection, this seems to be the least bad option.
408  id partialMockVC = OCMPartialMock(realVC);
409  OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
410 
411  // Exercise behavior under test.
412  [partialMockVC traitCollectionDidChange:nil];
413 
414  // Verify behavior.
415  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
416  return [message[@"platformBrightness"] isEqualToString:@"dark"];
417  }]]);
418 
419  // Clean up mocks
420  [partialMockVC stopMocking];
421  [settingsChannel stopMocking];
422  [mockTraitCollection stopMocking];
423 }
424 
425 // Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
426 // which is set to the given "style".
427 - (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
428  id mockTraitCollection = OCMClassMock([UITraitCollection class]);
429  OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
430  return mockTraitCollection;
431 }
432 
433 #pragma mark - Platform Contrast
434 
435 - (void)testItReportsNormalPlatformContrastByDefault {
436  if (@available(iOS 13, *)) {
437  // noop
438  } else {
439  return;
440  }
441 
442  // Setup test.
443  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
444  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
445 
446  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
447  nibName:nil
448  bundle:nil];
449 
450  // Exercise behavior under test.
451  [vc traitCollectionDidChange:nil];
452 
453  // Verify behavior.
454  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
455  return [message[@"platformContrast"] isEqualToString:@"normal"];
456  }]]);
457 
458  // Clean up mocks
459  [settingsChannel stopMocking];
460 }
461 
462 - (void)testItReportsPlatformContrastWhenViewWillAppear {
463  if (@available(iOS 13, *)) {
464  // noop
465  } else {
466  return;
467  }
468  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
469  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
470 
471  // Setup test.
472  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
473  OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
474  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
475  nibName:nil
476  bundle:nil];
477 
478  // Exercise behavior under test.
479  [vc viewWillAppear:false];
480 
481  // Verify behavior.
482  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
483  return [message[@"platformContrast"] isEqualToString:@"normal"];
484  }]]);
485 
486  // Clean up mocks
487  [settingsChannel stopMocking];
488 }
489 
490 - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
491  if (@available(iOS 13, *)) {
492  // noop
493  } else {
494  return;
495  }
496 
497  // Setup test.
498  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
499  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
500 
501  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
502  nibName:nil
503  bundle:nil];
504  id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];
505 
506  // We partially mock the real FlutterViewController to act as the OS and report
507  // the UITraitCollection of our choice. Mocking the object under test is not
508  // desirable, but given that the OS does not offer a DI approach to providing
509  // our own UITraitCollection, this seems to be the least bad option.
510  id partialMockVC = OCMPartialMock(realVC);
511  OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
512 
513  // Exercise behavior under test.
514  [partialMockVC traitCollectionDidChange:mockTraitCollection];
515 
516  // Verify behavior.
517  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
518  return [message[@"platformContrast"] isEqualToString:@"high"];
519  }]]);
520 
521  // Clean up mocks
522  [partialMockVC stopMocking];
523  [settingsChannel stopMocking];
524  [mockTraitCollection stopMocking];
525 }
526 
527 - (void)testPerformOrientationUpdateForcesOrientationChange {
528  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
529  currentOrientation:UIInterfaceOrientationLandscapeLeft
530  didChangeOrientation:YES
531  resultingOrientation:UIInterfaceOrientationPortrait];
532 
533  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
534  currentOrientation:UIInterfaceOrientationLandscapeRight
535  didChangeOrientation:YES
536  resultingOrientation:UIInterfaceOrientationPortrait];
537 
538  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
539  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
540  didChangeOrientation:YES
541  resultingOrientation:UIInterfaceOrientationPortrait];
542 
543  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
544  currentOrientation:UIInterfaceOrientationLandscapeLeft
545  didChangeOrientation:YES
546  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
547 
548  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
549  currentOrientation:UIInterfaceOrientationLandscapeRight
550  didChangeOrientation:YES
551  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
552 
553  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
554  currentOrientation:UIInterfaceOrientationPortrait
555  didChangeOrientation:YES
556  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
557 
558  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
559  currentOrientation:UIInterfaceOrientationPortrait
560  didChangeOrientation:YES
561  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
562 
563  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
564  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
565  didChangeOrientation:YES
566  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
567 
568  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
569  currentOrientation:UIInterfaceOrientationPortrait
570  didChangeOrientation:YES
571  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
572 
573  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
574  currentOrientation:UIInterfaceOrientationLandscapeRight
575  didChangeOrientation:YES
576  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
577 
578  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
579  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
580  didChangeOrientation:YES
581  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
582 
583  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
584  currentOrientation:UIInterfaceOrientationPortrait
585  didChangeOrientation:YES
586  resultingOrientation:UIInterfaceOrientationLandscapeRight];
587 
588  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
589  currentOrientation:UIInterfaceOrientationLandscapeLeft
590  didChangeOrientation:YES
591  resultingOrientation:UIInterfaceOrientationLandscapeRight];
592 
593  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
594  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
595  didChangeOrientation:YES
596  resultingOrientation:UIInterfaceOrientationLandscapeRight];
597 
598  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
599  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
600  didChangeOrientation:YES
601  resultingOrientation:UIInterfaceOrientationPortrait];
602 }
603 
604 - (void)testPerformOrientationUpdateDoesNotForceOrientationChange {
605  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
606  currentOrientation:UIInterfaceOrientationPortrait
607  didChangeOrientation:NO
608  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
609 
610  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
611  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
612  didChangeOrientation:NO
613  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
614 
615  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
616  currentOrientation:UIInterfaceOrientationLandscapeLeft
617  didChangeOrientation:NO
618  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
619 
620  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
621  currentOrientation:UIInterfaceOrientationLandscapeRight
622  didChangeOrientation:NO
623  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
624 
625  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
626  currentOrientation:UIInterfaceOrientationPortrait
627  didChangeOrientation:NO
628  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
629 
630  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
631  currentOrientation:UIInterfaceOrientationLandscapeLeft
632  didChangeOrientation:NO
633  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
634 
635  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
636  currentOrientation:UIInterfaceOrientationLandscapeRight
637  didChangeOrientation:NO
638  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
639 
640  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
641  currentOrientation:UIInterfaceOrientationPortrait
642  didChangeOrientation:NO
643  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
644 
645  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
646  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
647  didChangeOrientation:NO
648  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
649 
650  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
651  currentOrientation:UIInterfaceOrientationLandscapeLeft
652  didChangeOrientation:NO
653  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
654 
655  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
656  currentOrientation:UIInterfaceOrientationLandscapeRight
657  didChangeOrientation:NO
658  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
659 
660  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
661  currentOrientation:UIInterfaceOrientationLandscapeLeft
662  didChangeOrientation:NO
663  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
664 
665  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
666  currentOrientation:UIInterfaceOrientationLandscapeRight
667  didChangeOrientation:NO
668  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
669 }
670 
671 // Perform an orientation update test that fails when the expected outcome
672 // for an orientation update is not met
673 - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask
674  currentOrientation:(UIInterfaceOrientation)currentOrientation
675  didChangeOrientation:(BOOL)didChange
676  resultingOrientation:(UIInterfaceOrientation)resultingOrientation {
677  id deviceMock = OCMPartialMock([UIDevice currentDevice]);
678  if (!didChange) {
679  OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]);
680  } else {
681  OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]);
682  }
683 
684  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
685  nibName:nil
686  bundle:nil];
687  id mockApplication = OCMClassMock([UIApplication class]);
688  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
689  OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation);
690 
691  [realVC performOrientationUpdate:mask];
692  OCMVerifyAll(deviceMock);
693  [deviceMock stopMocking];
694  [mockApplication stopMocking];
695 }
696 
697 // Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
698 // which is set to the given "contrast".
699 - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
700  id mockTraitCollection = OCMClassMock([UITraitCollection class]);
701  OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
702  return mockTraitCollection;
703 }
704 
705 - (void)testWillDeallocNotification {
706  XCTestExpectation* expectation =
707  [[XCTestExpectation alloc] initWithDescription:@"notification called"];
708  id engine = [[MockEngine alloc] init];
709  @autoreleasepool {
710  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
711  nibName:nil
712  bundle:nil];
713  [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc
714  object:nil
715  queue:[NSOperationQueue mainQueue]
716  usingBlock:^(NSNotification* _Nonnull note) {
717  [expectation fulfill];
718  }];
719  realVC = nil;
720  }
721  [self waitForExpectations:@[ expectation ] timeout:1.0];
722 }
723 
724 - (void)testDoesntLoadViewInInit {
725  FlutterDartProject* project = [[FlutterDartProject alloc] init];
726  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
727  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
728  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
729  nibName:nil
730  bundle:nil];
731  XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown");
732  engine.viewController = nil;
733 }
734 
735 - (void)testHideOverlay {
736  FlutterDartProject* project = [[FlutterDartProject alloc] init];
737  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
738  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
739  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
740  nibName:nil
741  bundle:nil];
742  XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @"");
743  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator
744  object:nil];
745  XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @"");
746  engine.viewController = nil;
747 }
748 
749 - (void)testNotifyLowMemory {
751  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
752  nibName:nil
753  bundle:nil];
754  id viewControllerMock = OCMPartialMock(viewController);
755  OCMStub([viewControllerMock surfaceUpdated:NO]);
756  [viewController beginAppearanceTransition:NO animated:NO];
757  [viewController endAppearanceTransition];
758  XCTAssertTrue(mockEngine.didCallNotifyLowMemory);
759 }
760 
761 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
762  NSMutableDictionary* replyMessage = [@{
763  @"handled" : @YES,
764  } mutableCopy];
765  // Response is async, so we have to post it to the run loop instead of calling
766  // it directly.
767  self.messageSent = message;
768  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
769  ^() {
770  callback(replyMessage);
771  });
772 }
773 
774 - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
775  if (@available(iOS 13.4, *)) {
776  // noop
777  } else {
778  return;
779  }
781  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
782  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
783  .andCall(self, @selector(sendMessage:reply:));
784  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
785  mockEngine.textInputPlugin = self.mockTextInputPlugin;
786 
787  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
788  nibName:nil
789  bundle:nil];
790 
791  // Allocate the keyboard manager in the view controller by adding the internal
792  // plugins.
793  [vc addInternalPlugins];
794 
795  [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0)
796  nextAction:^(){
797  }];
798 
799  XCTAssert(self.messageSent != nil);
800  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
801  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]);
802  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
803  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
804  XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]);
805  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]);
806  [vc deregisterNotifications];
807 }
808 
809 - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
810  if (@available(iOS 13.4, *)) {
811  // noop
812  } else {
813  return;
814  }
815 
817  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
818  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
819  .andCall(self, @selector(sendMessage:reply:));
820  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
821  mockEngine.textInputPlugin = self.mockTextInputPlugin;
822 
823  __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
824  nibName:nil
825  bundle:nil];
826  // Allocate the keyboard manager in the view controller by adding the internal
827  // plugins.
828  [vc addInternalPlugins];
829 
830  [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A",
831  "a")
832  nextAction:^(){
833  }];
834 
835  XCTAssert(self.messageSent != nil);
836  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
837  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]);
838  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
839  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
840  XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]);
841  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]);
842  [vc deregisterNotifications];
843  vc = nil;
844 }
845 
846 - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
847  if (@available(iOS 13.4, *)) {
848  // noop
849  } else {
850  return;
851  }
852  id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
853  OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
854  .andCall(self, @selector(sendMessage:reply:));
855  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
856  OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel);
857 
858  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
859  nibName:nil
860  bundle:nil];
861 
862  // Allocate the keyboard manager in the view controller by adding the internal
863  // plugins.
864  [vc addInternalPlugins];
865 
866  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA,
867  UIKeyModifierShift, 123.0)
868  nextAction:^(){
869  }];
870  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA,
871  UIKeyModifierShift, 123.0)
872  nextAction:^(){
873  }];
874  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA,
875  UIKeyModifierShift, 123.0)
876  nextAction:^(){
877  }];
878 
879  XCTAssert(self.messageSent == nil);
880  OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
881  [vc deregisterNotifications];
882 }
883 
884 - (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) {
885  if (@available(iOS 13.4, *)) {
886  // noop
887  } else {
888  return;
889  }
890 
891  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
892  nibName:nil
893  bundle:nil];
894  XCTAssertNotNil(vc);
895  UIView* view = vc.view;
896  XCTAssertNotNil(view);
897  NSArray* gestureRecognizers = view.gestureRecognizers;
898  XCTAssertNotNil(gestureRecognizers);
899 
900  BOOL found = NO;
901  for (id gesture in gestureRecognizers) {
902  if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
903  found = YES;
904  break;
905  }
906  }
907  XCTAssertTrue(found);
908 }
909 
910 - (void)testMouseSupport API_AVAILABLE(ios(13.4)) {
911  if (@available(iOS 13.4, *)) {
912  // noop
913  } else {
914  return;
915  }
916 
917  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
918  nibName:nil
919  bundle:nil];
920  XCTAssertNotNil(vc);
921 
922  id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
923  XCTAssertNotNil(mockPanGestureRecognizer);
924 
925  [vc scrollEvent:mockPanGestureRecognizer];
926 
927  [[[self.mockEngine verify] ignoringNonObjectArgs]
928  dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>()];
929 }
930 
931 @end
FlutterTextInputPlugin * textInputPlugin
Sometimes we have to use a custom mock to avoid retain cycles in ocmock.
fml::scoped_nsobject< UIPointerInteraction > _pointerInteraction API_AVAILABLE(ios(13.4))
static CFStringRef kMessageLoopCFRunLoopMode
FlKeyEvent FlKeyResponderAsyncCallback callback
FlutterBasicMessageChannel * lifecycleChannel
NSNotificationName const FlutterViewControllerWillDealloc
NSObject< FlutterBinaryMessenger > * binaryMessenger
FlutterTextInputPlugin * textInputPlugin
UIAccessibilityContrast accessibilityContrast()
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
int BOOL
Definition: windows_types.h:37
FlView * view
FlutterViewController * viewController
UIAccessibilityContrast
void(* FlutterKeyEventCallback)(bool, void *)
Definition: embedder.h:748
#define FLUTTER_ASSERT_ARC
Definition: FlutterMacros.h:44
FlutterViewController * viewController
FlutterBasicMessageChannel * keyEventChannel