Flutter iOS Embedder
FlutterEngineTest.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 <Foundation/Foundation.h>
6 #import <OCMock/OCMock.h>
7 #import <XCTest/XCTest.h>
8 
9 #import <objc/runtime.h>
10 
11 #import "flutter/common/settings.h"
12 #include "flutter/fml/synchronization/sync_switch.h"
23 
25 @end
26 
28 @property(nonatomic) BOOL ensureSemanticsEnabledCalled;
29 @end
30 
31 @implementation FlutterEngineSpy
32 
33 - (void)ensureSemanticsEnabled {
34  _ensureSemanticsEnabledCalled = YES;
35 }
36 
37 @end
38 
40 
41 @end
42 
43 /// FlutterBinaryMessengerRelay used for testing that setting FlutterEngine.binaryMessenger to
44 /// the current instance doesn't trigger a use-after-free bug.
45 ///
46 /// See: testSetBinaryMessengerToSameBinaryMessenger
48 @property(nonatomic, assign) BOOL failOnDealloc;
49 @end
50 
51 @implementation FakeBinaryMessengerRelay
52 - (void)dealloc {
53  if (_failOnDealloc) {
54  XCTFail("FakeBinaryMessageRelay should not be deallocated");
55  }
56 }
57 @end
58 
59 @interface FlutterEngineTest : XCTestCase
60 @end
61 
62 @implementation FlutterEngineTest
63 
64 - (void)setUp {
65 }
66 
67 - (void)tearDown {
68 }
69 
70 - (void)testCreate {
71  FlutterDartProject* project = [[FlutterDartProject alloc] init];
72  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
73  XCTAssertNotNil(engine);
74 }
75 
76 - (void)testShellGetters {
77  FlutterDartProject* project = [[FlutterDartProject alloc] init];
78  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
79  XCTAssertNotNil(engine);
80 
81  // Ensure getters don't deref _shell when it's null, and instead return nullptr.
82  XCTAssertEqual(engine.platformTaskRunner.get(), nullptr);
83  XCTAssertEqual(engine.uiTaskRunner.get(), nullptr);
84  XCTAssertEqual(engine.rasterTaskRunner.get(), nullptr);
85 }
86 
87 - (void)testInfoPlist {
88  // Check the embedded Flutter.framework Info.plist, not the linked dylib.
89  NSURL* flutterFrameworkURL =
90  [NSBundle.mainBundle.privateFrameworksURL URLByAppendingPathComponent:@"Flutter.framework"];
91  NSBundle* flutterBundle = [NSBundle bundleWithURL:flutterFrameworkURL];
92  XCTAssertEqualObjects(flutterBundle.bundleIdentifier, @"io.flutter.flutter");
93 
94  NSDictionary<NSString*, id>* infoDictionary = flutterBundle.infoDictionary;
95 
96  // OS version can have one, two, or three digits: "8", "8.0", "8.0.0"
97  NSError* regexError = NULL;
98  NSRegularExpression* osVersionRegex =
99  [NSRegularExpression regularExpressionWithPattern:@"((0|[1-9]\\d*)\\.)*(0|[1-9]\\d*)"
100  options:NSRegularExpressionCaseInsensitive
101  error:&regexError];
102  XCTAssertNil(regexError);
103 
104  // Smoke test the test regex.
105  NSString* testString = @"9";
106  NSUInteger versionMatches =
107  [osVersionRegex numberOfMatchesInString:testString
108  options:NSMatchingAnchored
109  range:NSMakeRange(0, testString.length)];
110  XCTAssertEqual(versionMatches, 1UL);
111  testString = @"9.1";
112  versionMatches = [osVersionRegex numberOfMatchesInString:testString
113  options:NSMatchingAnchored
114  range:NSMakeRange(0, testString.length)];
115  XCTAssertEqual(versionMatches, 1UL);
116  testString = @"9.0.1";
117  versionMatches = [osVersionRegex numberOfMatchesInString:testString
118  options:NSMatchingAnchored
119  range:NSMakeRange(0, testString.length)];
120  XCTAssertEqual(versionMatches, 1UL);
121  testString = @".0.1";
122  versionMatches = [osVersionRegex numberOfMatchesInString:testString
123  options:NSMatchingAnchored
124  range:NSMakeRange(0, testString.length)];
125  XCTAssertEqual(versionMatches, 0UL);
126 
127  // Test Info.plist values.
128  NSString* minimumOSVersion = infoDictionary[@"MinimumOSVersion"];
129  versionMatches = [osVersionRegex numberOfMatchesInString:minimumOSVersion
130  options:NSMatchingAnchored
131  range:NSMakeRange(0, minimumOSVersion.length)];
132  XCTAssertEqual(versionMatches, 1UL);
133 
134  // SHA length is 40.
135  XCTAssertEqual(((NSString*)infoDictionary[@"FlutterEngine"]).length, 40UL);
136 
137  // {clang_version} placeholder is 15 characters. The clang string version
138  // is longer than that, so check if the placeholder has been replaced, without
139  // actually checking a literal string, which could be different on various machines.
140  XCTAssertTrue(((NSString*)infoDictionary[@"ClangVersion"]).length > 15UL);
141 }
142 
143 - (void)testDeallocated {
144  __weak FlutterEngine* weakEngine = nil;
145  @autoreleasepool {
146  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
147  weakEngine = engine;
148  [engine run];
149  XCTAssertNotNil(weakEngine);
150  }
151  XCTAssertNil(weakEngine);
152 }
153 
154 - (void)testSendMessageBeforeRun {
155  FlutterDartProject* project = [[FlutterDartProject alloc] init];
156  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
157  XCTAssertNotNil(engine);
158  XCTAssertThrows([engine.binaryMessenger
159  sendOnChannel:@"foo"
160  message:[@"bar" dataUsingEncoding:NSUTF8StringEncoding]
161  binaryReply:nil]);
162 }
163 
164 - (void)testSetMessageHandlerBeforeRun {
165  FlutterDartProject* project = [[FlutterDartProject alloc] init];
166  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
167  XCTAssertNotNil(engine);
168  XCTAssertThrows([engine.binaryMessenger
169  setMessageHandlerOnChannel:@"foo"
170  binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply){
171 
172  }]);
173 }
174 
175 - (void)testNilSetMessageHandlerBeforeRun {
176  FlutterDartProject* project = [[FlutterDartProject alloc] init];
177  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
178  XCTAssertNotNil(engine);
179  XCTAssertNoThrow([engine.binaryMessenger setMessageHandlerOnChannel:@"foo"
180  binaryMessageHandler:nil]);
181 }
182 
183 - (void)testNotifyPluginOfDealloc {
184  id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
185  OCMStub([plugin detachFromEngineForRegistrar:[OCMArg any]]);
186  @autoreleasepool {
187  FlutterDartProject* project = [[FlutterDartProject alloc] init];
188  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
189  NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"plugin"];
190  [registrar publish:plugin];
191  }
192  OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]);
193 }
194 
195 - (void)testGetViewControllerFromRegistrar {
196  FlutterDartProject* project = [[FlutterDartProject alloc] init];
197  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
198  id mockEngine = OCMPartialMock(engine);
199  NSObject<FlutterPluginRegistrar>* registrar = [mockEngine registrarForPlugin:@"plugin"];
200 
201  // Verify accessing the viewController getter calls FlutterEngine.viewController.
202  (void)[registrar viewController];
203  OCMVerify(times(1), [mockEngine viewController]);
204 }
205 
206 - (void)testSetBinaryMessengerToSameBinaryMessenger {
207  FakeBinaryMessengerRelay* fakeBinaryMessenger = [[FakeBinaryMessengerRelay alloc] init];
208 
209  FlutterEngine* engine = [[FlutterEngine alloc] init];
210  [engine setBinaryMessenger:fakeBinaryMessenger];
211 
212  // Verify that the setter doesn't free the old messenger before setting the new messenger.
213  fakeBinaryMessenger.failOnDealloc = YES;
214  [engine setBinaryMessenger:fakeBinaryMessenger];
215 
216  // Don't fail when ARC releases the binary messenger.
217  fakeBinaryMessenger.failOnDealloc = NO;
218 }
219 
220 - (void)testRunningInitialRouteSendsNavigationMessage {
221  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
222 
223  FlutterEngine* engine = [[FlutterEngine alloc] init];
224  [engine setBinaryMessenger:mockBinaryMessenger];
225 
226  // Run with an initial route.
227  [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
228 
229  // Now check that an encoded method call has been made on the binary messenger to set the
230  // initial route to "test".
231  FlutterMethodCall* setInitialRouteMethodCall =
232  [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
233  NSData* encodedSetInitialRouteMethod =
234  [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
235  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
236  message:encodedSetInitialRouteMethod]);
237 }
238 
239 - (void)testInitialRouteSettingsSendsNavigationMessage {
240  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
241 
242  auto settings = FLTDefaultSettingsForBundle();
243  settings.route = "test";
244  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
245  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
246  [engine setBinaryMessenger:mockBinaryMessenger];
247  [engine run];
248 
249  // Now check that an encoded method call has been made on the binary messenger to set the
250  // initial route to "test".
251  FlutterMethodCall* setInitialRouteMethodCall =
252  [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
253  NSData* encodedSetInitialRouteMethod =
254  [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
255  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
256  message:encodedSetInitialRouteMethod]);
257 }
258 
259 - (void)testPlatformViewsControllerRenderingMetalBackend {
260  FlutterEngine* engine = [[FlutterEngine alloc] init];
261  [engine run];
262  flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
263 
264  XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kMetal);
265 }
266 
267 - (void)testWaitForFirstFrameTimeout {
268  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
269  [engine run];
270  XCTestExpectation* timeoutFirstFrame = [self expectationWithDescription:@"timeoutFirstFrame"];
271  [engine waitForFirstFrame:0.1
272  callback:^(BOOL didTimeout) {
273  if (timeoutFirstFrame) {
274  [timeoutFirstFrame fulfill];
275  }
276  }];
277  [self waitForExpectations:@[ timeoutFirstFrame ]];
278 }
279 
280 - (void)testSpawn {
281  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
282  [engine run];
283  FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
284  libraryURI:nil
285  initialRoute:nil
286  entrypointArgs:nil];
287  XCTAssertNotNil(spawn);
288 }
289 
290 - (void)testEngineId {
291  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
292  [engine run];
293  int64_t id1 = engine.engineIdentifier;
294  XCTAssertTrue(id1 != 0);
295  FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
296  libraryURI:nil
297  initialRoute:nil
298  entrypointArgs:nil];
299  int64_t id2 = spawn.engineIdentifier;
300  XCTAssertEqual([FlutterEngine engineForIdentifier:id1], engine);
301  XCTAssertEqual([FlutterEngine engineForIdentifier:id2], spawn);
302 }
303 
304 - (void)testSetHandlerAfterRun {
305  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
306  XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];
307  dispatch_async(dispatch_get_main_queue(), ^{
308  NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"foo"];
309  fml::AutoResetWaitableEvent latch;
310  [engine run];
311  flutter::Shell& shell = engine.shell;
312  fml::TaskRunner::RunNowOrPostTask(
313  engine.shell.GetTaskRunners().GetUITaskRunner(), [&latch, &shell] {
314  flutter::Engine::Delegate& delegate = shell;
315  auto message = std::make_unique<flutter::PlatformMessage>("foo", nullptr);
316  delegate.OnEngineHandlePlatformMessage(std::move(message));
317  latch.Signal();
318  });
319  latch.Wait();
320  [registrar.messenger setMessageHandlerOnChannel:@"foo"
321  binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) {
322  [gotMessage fulfill];
323  }];
324  });
325  [self waitForExpectations:@[ gotMessage ]];
326 }
327 
328 - (void)testThreadPrioritySetCorrectly {
329  XCTestExpectation* prioritiesSet = [self expectationWithDescription:@"prioritiesSet"];
330  prioritiesSet.expectedFulfillmentCount = 2;
331 
332  IMP mockSetThreadPriority =
333  imp_implementationWithBlock(^(NSThread* thread, double threadPriority) {
334  if ([thread.name hasSuffix:@".raster"]) {
335  XCTAssertEqual(threadPriority, 1.0);
336  [prioritiesSet fulfill];
337  } else if ([thread.name hasSuffix:@".io"]) {
338  XCTAssertEqual(threadPriority, 0.5);
339  [prioritiesSet fulfill];
340  }
341  });
342  Method method = class_getInstanceMethod([NSThread class], @selector(setThreadPriority:));
343  IMP originalSetThreadPriority = method_getImplementation(method);
344  method_setImplementation(method, mockSetThreadPriority);
345 
346  FlutterEngine* engine = [[FlutterEngine alloc] init];
347  [engine run];
348  [self waitForExpectations:@[ prioritiesSet ]];
349 
350  method_setImplementation(method, originalSetThreadPriority);
351 }
352 
353 - (void)testCanEnableDisableEmbedderAPIThroughInfoPlist {
354  {
355  // Not enable embedder API by default
356  auto settings = FLTDefaultSettingsForBundle();
357  settings.enable_software_rendering = true;
358  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
359  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
360  XCTAssertFalse(engine.enableEmbedderAPI);
361  }
362  {
363  // Enable embedder api
364  id mockMainBundle = OCMPartialMock([NSBundle mainBundle]);
365  OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"])
366  .andReturn(@"YES");
367  auto settings = FLTDefaultSettingsForBundle();
368  settings.enable_software_rendering = true;
369  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
370  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
371  XCTAssertTrue(engine.enableEmbedderAPI);
372  }
373 }
374 
375 - (void)testFlutterTextInputViewDidResignFirstResponderWillCallTextInputClientConnectionClosed {
376  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
377  FlutterEngine* engine = [[FlutterEngine alloc] init];
378  [engine setBinaryMessenger:mockBinaryMessenger];
379  [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
380  [engine flutterTextInputView:nil didResignFirstResponderWithTextInputClient:1];
381  FlutterMethodCall* methodCall =
382  [FlutterMethodCall methodCallWithMethodName:@"TextInputClient.onConnectionClosed"
383  arguments:@[ @(1) ]];
384  NSData* encodedMethodCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:methodCall];
385  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/textinput" message:encodedMethodCall]);
386 }
387 
388 - (void)testFlutterEngineUpdatesDisplays {
389  FlutterEngine* engine = [[FlutterEngine alloc] init];
390  id mockEngine = OCMPartialMock(engine);
391 
392  [engine run];
393  OCMVerify(times(1), [mockEngine updateDisplays]);
394  engine.viewController = nil;
395  OCMVerify(times(2), [mockEngine updateDisplays]);
396 }
397 
398 - (void)testLifeCycleNotificationDidEnterBackgroundForApplication {
399  FlutterDartProject* project = [[FlutterDartProject alloc] init];
400  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
401  [engine run];
402  NSNotification* sceneNotification =
403  [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
404  object:nil
405  userInfo:nil];
406  NSNotification* applicationNotification =
407  [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
408  object:nil
409  userInfo:nil];
410  id mockEngine = OCMPartialMock(engine);
411  [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
412  [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
413  OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]);
414  XCTAssertTrue(engine.isGpuDisabled);
415  BOOL gpuDisabled = NO;
416  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
417  fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] {
418  gpuDisabled = NO;
419  }));
420  XCTAssertTrue(gpuDisabled);
421 }
422 
423 - (void)testLifeCycleNotificationDidEnterBackgroundForScene {
424  id mockBundle = OCMPartialMock([NSBundle mainBundle]);
425  OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
426  @"NSExtensionPointIdentifier" : @"com.apple.share-services"
427  });
428  FlutterDartProject* project = [[FlutterDartProject alloc] init];
429  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
430  [engine run];
431  NSNotification* sceneNotification =
432  [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
433  object:nil
434  userInfo:nil];
435  NSNotification* applicationNotification =
436  [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
437  object:nil
438  userInfo:nil];
439  id mockEngine = OCMPartialMock(engine);
440  [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
441  [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
442  OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]);
443  XCTAssertTrue(engine.isGpuDisabled);
444  BOOL gpuDisabled = NO;
445  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
446  fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] {
447  gpuDisabled = NO;
448  }));
449  XCTAssertTrue(gpuDisabled);
450  [mockBundle stopMocking];
451 }
452 
453 - (void)testLifeCycleNotificationWillEnterForegroundForApplication {
454  FlutterDartProject* project = [[FlutterDartProject alloc] init];
455  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
456  [engine run];
457  NSNotification* sceneNotification =
458  [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
459  object:nil
460  userInfo:nil];
461  NSNotification* applicationNotification =
462  [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
463  object:nil
464  userInfo:nil];
465  id mockEngine = OCMPartialMock(engine);
466  [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
467  [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
468  OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]);
469  XCTAssertFalse(engine.isGpuDisabled);
470  BOOL gpuDisabled = YES;
471  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
472  fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] {
473  gpuDisabled = NO;
474  }));
475  XCTAssertFalse(gpuDisabled);
476 }
477 
478 - (void)testLifeCycleNotificationWillEnterForegroundForScene {
479  id mockBundle = OCMPartialMock([NSBundle mainBundle]);
480  OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
481  @"NSExtensionPointIdentifier" : @"com.apple.share-services"
482  });
483  FlutterDartProject* project = [[FlutterDartProject alloc] init];
484  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
485  [engine run];
486  NSNotification* sceneNotification =
487  [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
488  object:nil
489  userInfo:nil];
490  NSNotification* applicationNotification =
491  [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
492  object:nil
493  userInfo:nil];
494  id mockEngine = OCMPartialMock(engine);
495  [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
496  [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
497  OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]);
498  XCTAssertFalse(engine.isGpuDisabled);
499  BOOL gpuDisabled = YES;
500  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
501  fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] {
502  gpuDisabled = NO;
503  }));
504  XCTAssertFalse(gpuDisabled);
505  [mockBundle stopMocking];
506 }
507 
508 - (void)testLifeCycleNotificationSceneWillConnect {
509  FlutterDartProject* project = [[FlutterDartProject alloc] init];
510  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
511  [engine run];
512  id mockScene = OCMClassMock([UIWindowScene class]);
513  id mockLifecycleProvider = OCMProtocolMock(@protocol(FlutterSceneLifeCycleProvider));
514  id mockLifecycleDelegate = OCMClassMock([FlutterPluginSceneLifeCycleDelegate class]);
515  OCMStub([mockScene delegate]).andReturn(mockLifecycleProvider);
516  OCMStub([mockLifecycleProvider sceneLifeCycleDelegate]).andReturn(mockLifecycleDelegate);
517 
518  NSNotification* sceneNotification =
519  [NSNotification notificationWithName:UISceneWillConnectNotification
520  object:mockScene
521  userInfo:nil];
522 
523  [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
524  OCMVerify(times(1), [mockLifecycleDelegate engine:engine
525  receivedConnectNotificationFor:mockScene]);
526 }
527 
528 - (void)testSpawnsShareGpuContext {
529  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
530  [engine run];
531  FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
532  libraryURI:nil
533  initialRoute:nil
534  entrypointArgs:nil];
535  XCTAssertNotNil(spawn);
536  XCTAssertTrue(engine.platformView != nullptr);
537  XCTAssertTrue(spawn.platformView != nullptr);
538  std::shared_ptr<flutter::IOSContext> engine_context = engine.platformView->GetIosContext();
539  std::shared_ptr<flutter::IOSContext> spawn_context = spawn.platformView->GetIosContext();
540  XCTAssertEqual(engine_context, spawn_context);
541 }
542 
543 - (void)testEnableSemanticsWhenFlutterViewAccessibilityDidCall {
544  FlutterEngineSpy* engine = [[FlutterEngineSpy alloc] initWithName:@"foobar"];
545  engine.ensureSemanticsEnabledCalled = NO;
546  [engine flutterViewAccessibilityDidCall];
547  XCTAssertTrue(engine.ensureSemanticsEnabledCalled);
548 }
549 
550 - (void)testCanMergePlatformAndUIThread {
551 #if defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR
552  auto settings = FLTDefaultSettingsForBundle();
553  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
554  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
555  [engine run];
556 
557  XCTAssertEqual(engine.shell.GetTaskRunners().GetUITaskRunner(),
558  engine.shell.GetTaskRunners().GetPlatformTaskRunner());
559 #endif // defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR
560 }
561 
562 - (void)testCanUnMergePlatformAndUIThread {
563 #if defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR
564  auto settings = FLTDefaultSettingsForBundle();
565  settings.merged_platform_ui_thread = flutter::Settings::MergedPlatformUIThread::kDisabled;
566  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
567  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
568  [engine run];
569 
570  XCTAssertNotEqual(engine.shell.GetTaskRunners().GetUITaskRunner(),
571  engine.shell.GetTaskRunners().GetPlatformTaskRunner());
572 #endif // defined(TARGET_IPHONE_SIMULATOR) && TARGET_IPHONE_SIMULATOR
573 }
574 
575 - (void)testAddSceneDelegateToRegistrar {
576  FlutterDartProject* project = [[FlutterDartProject alloc] init];
577  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
578  id mockEngine = OCMPartialMock(engine);
579  NSObject<FlutterPluginRegistrar>* registrar = [mockEngine registrarForPlugin:@"plugin"];
580  id mockPlugin = OCMProtocolMock(@protocol(TestFlutterPluginWithSceneEvents));
581  [registrar addSceneDelegate:mockPlugin];
582 
583  OCMVerify(times(1), [mockEngine addSceneLifeCycleDelegate:[OCMArg any]]);
584 }
585 
586 - (void)testNotifyAppDelegateOfEngineInitialization {
587  FlutterDartProject* project = [[FlutterDartProject alloc] init];
588  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
589 
590  id mockApplication = OCMClassMock([UIApplication class]);
591  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
592  id mockAppDelegate = OCMProtocolMock(@protocol(FlutterImplicitEngineDelegate));
593  OCMStub([mockApplication delegate]).andReturn(mockAppDelegate);
594 
595  [engine performImplicitEngineCallback];
596  OCMVerify(times(1), [mockAppDelegate didInitializeImplicitFlutterEngine:[OCMArg any]]);
597 }
598 
599 - (void)testRegistrarForPlugin {
600  FlutterDartProject* project = [[FlutterDartProject alloc] init];
601  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
602  FlutterEngine* mockEngine = OCMPartialMock(engine);
603  id mockViewController = OCMClassMock([FlutterViewController class]);
604  id mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
605  id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry));
606  id mockPlatformViewController = OCMClassMock([FlutterPlatformViewsController class]);
607  OCMStub([mockEngine viewController]).andReturn(mockViewController);
608  OCMStub([mockEngine binaryMessenger]).andReturn(mockBinaryMessenger);
609  OCMStub([mockEngine textureRegistry]).andReturn(mockTextureRegistry);
610  OCMStub([mockEngine platformViewsController]).andReturn(mockPlatformViewController);
611 
612  NSString* pluginKey = @"plugin";
613  NSString* assetKey = @"asset";
614  NSString* factoryKey = @"platform_view_factory";
615 
616  NSObject<FlutterPluginRegistrar>* registrar = [mockEngine registrarForPlugin:pluginKey];
617 
618  XCTAssertTrue([registrar respondsToSelector:@selector(messenger)]);
619  XCTAssertTrue([registrar respondsToSelector:@selector(textures)]);
620  XCTAssertTrue([registrar respondsToSelector:@selector(registerViewFactory:withId:)]);
621  XCTAssertTrue([registrar
622  respondsToSelector:@selector(registerViewFactory:withId:gestureRecognizersBlockingPolicy:)]);
623  XCTAssertTrue([registrar respondsToSelector:@selector(viewController)]);
624  XCTAssertTrue([registrar respondsToSelector:@selector(publish:)]);
625  XCTAssertTrue([registrar respondsToSelector:@selector(addMethodCallDelegate:channel:)]);
626  XCTAssertTrue([registrar respondsToSelector:@selector(addApplicationDelegate:)]);
627  XCTAssertTrue([registrar respondsToSelector:@selector(lookupKeyForAsset:)]);
628  XCTAssertTrue([registrar respondsToSelector:@selector(lookupKeyForAsset:fromPackage:)]);
629 
630  // Verify messenger, textures, and viewController forwards to FlutterEngine
631  XCTAssertEqual(registrar.messenger, mockBinaryMessenger);
632  XCTAssertEqual(registrar.textures, mockTextureRegistry);
633  XCTAssertEqual(registrar.viewController, mockViewController);
634 
635  // Verify registerViewFactory:withId:, registerViewFactory:withId:gestureRecognizersBlockingPolicy
636  // forwards to FlutterEngine
637  id mockPlatformViewFactory = OCMProtocolMock(@protocol(FlutterPlatformViewFactory));
638  [registrar registerViewFactory:mockPlatformViewFactory withId:factoryKey];
639  [registrar registerViewFactory:mockPlatformViewFactory
640  withId:factoryKey
641  gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
642  OCMVerify(times(2), [mockPlatformViewController registerViewFactory:mockPlatformViewFactory
643  withId:factoryKey
644  gestureRecognizersBlockingPolicy:
646 
647  // Verify publish forwards to FlutterEngine
648  id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
649  [registrar publish:plugin];
650  XCTAssertEqual(mockEngine.pluginPublications[pluginKey], plugin);
651 
652  // Verify lookupKeyForAsset:, lookupKeyForAsset:fromPackage forward to engine
653  [registrar lookupKeyForAsset:assetKey];
654  OCMVerify(times(1), [mockEngine lookupKeyForAsset:assetKey]);
655  [registrar lookupKeyForAsset:assetKey fromPackage:pluginKey];
656  OCMVerify(times(1), [mockEngine lookupKeyForAsset:assetKey fromPackage:pluginKey]);
657 }
658 
659 - (void)testRegistrarForApplication {
660  FlutterDartProject* project = [[FlutterDartProject alloc] init];
661  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
662  FlutterEngine* mockEngine = OCMPartialMock(engine);
663  id mockViewController = OCMClassMock([FlutterViewController class]);
664  id mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
665  id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry));
666  id mockPlatformViewController = OCMClassMock([FlutterPlatformViewsController class]);
667  OCMStub([mockEngine viewController]).andReturn(mockViewController);
668  OCMStub([mockEngine binaryMessenger]).andReturn(mockBinaryMessenger);
669  OCMStub([mockEngine textureRegistry]).andReturn(mockTextureRegistry);
670  OCMStub([mockEngine platformViewsController]).andReturn(mockPlatformViewController);
671 
672  NSString* pluginKey = @"plugin";
673  NSString* factoryKey = @"platform_view_factory";
674 
675  NSObject<FlutterApplicationRegistrar>* registrar = [mockEngine registrarForApplication:pluginKey];
676 
677  XCTAssertTrue([registrar respondsToSelector:@selector(messenger)]);
678  XCTAssertTrue([registrar respondsToSelector:@selector(textures)]);
679  XCTAssertTrue([registrar respondsToSelector:@selector(registerViewFactory:withId:)]);
680  XCTAssertTrue([registrar
681  respondsToSelector:@selector(registerViewFactory:withId:gestureRecognizersBlockingPolicy:)]);
682  XCTAssertFalse([registrar respondsToSelector:@selector(viewController)]);
683  XCTAssertFalse([registrar respondsToSelector:@selector(publish:)]);
684  XCTAssertFalse([registrar respondsToSelector:@selector(addMethodCallDelegate:channel:)]);
685  XCTAssertFalse([registrar respondsToSelector:@selector(addApplicationDelegate:)]);
686  XCTAssertFalse([registrar respondsToSelector:@selector(lookupKeyForAsset:)]);
687  XCTAssertFalse([registrar respondsToSelector:@selector(lookupKeyForAsset:fromPackage:)]);
688 
689  // Verify messenger and textures forwards to FlutterEngine
690  XCTAssertEqual(registrar.messenger, mockBinaryMessenger);
691  XCTAssertEqual(registrar.textures, mockTextureRegistry);
692 
693  // Verify registerViewFactory:withId:, registerViewFactory:withId:gestureRecognizersBlockingPolicy
694  // forwards to FlutterEngine
695  id mockPlatformViewFactory = OCMProtocolMock(@protocol(FlutterPlatformViewFactory));
696  [registrar registerViewFactory:mockPlatformViewFactory withId:factoryKey];
697  [registrar registerViewFactory:mockPlatformViewFactory
698  withId:factoryKey
699  gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
700  OCMVerify(times(2), [mockPlatformViewController registerViewFactory:mockPlatformViewFactory
701  withId:factoryKey
702  gestureRecognizersBlockingPolicy:
704 }
705 
706 - (void)testSendDeepLinkToFrameworkTimesOut {
707  FlutterDartProject* project = [[FlutterDartProject alloc] init];
708  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
709  id mockEngine = OCMPartialMock(engine);
710  id mockEngineFirstFrameCallback = [OCMArg invokeBlockWithArgs:@YES, nil];
711  OCMStub([mockEngine waitForFirstFrame:3.0 callback:mockEngineFirstFrameCallback]);
712 
713  NSURL* url = [NSURL URLWithString:@"example.com"];
714 
715  [mockEngine sendDeepLinkToFramework:url
716  completionHandler:^(BOOL success) {
717  XCTAssertFalse(success);
718  }];
719 }
720 
721 - (void)testSendDeepLinkToFrameworkUsingNavigationChannel {
722  NSString* urlString = @"example.com";
723  NSURL* url = [NSURL URLWithString:urlString];
724  FlutterDartProject* project = [[FlutterDartProject alloc] init];
725  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
726  id mockEngine = OCMPartialMock(engine);
727  id mockEngineFirstFrameCallback = [OCMArg invokeBlockWithArgs:@NO, nil];
728  OCMStub([mockEngine waitForFirstFrame:3.0 callback:mockEngineFirstFrameCallback]);
729  id mockNavigationChannel = OCMClassMock([FlutterMethodChannel class]);
730  OCMStub([mockEngine navigationChannel]).andReturn(mockNavigationChannel);
731  id mockNavigationChannelCallback = [OCMArg invokeBlockWithArgs:@1, nil];
732  OCMStub([mockNavigationChannel invokeMethod:@"pushRouteInformation"
733  arguments:@{@"location" : urlString}
734  result:mockNavigationChannelCallback]);
735 
736  [mockEngine sendDeepLinkToFramework:url
737  completionHandler:^(BOOL success) {
738  XCTAssertTrue(success);
739  }];
740 }
741 
742 - (void)testSendDeepLinkToFrameworkUsingNavigationChannelFails {
743  NSString* urlString = @"example.com";
744  NSURL* url = [NSURL URLWithString:urlString];
745  FlutterDartProject* project = [[FlutterDartProject alloc] init];
746  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
747  id mockEngine = OCMPartialMock(engine);
748  id mockEngineFirstFrameCallback = [OCMArg invokeBlockWithArgs:@NO, nil];
749  OCMStub([mockEngine waitForFirstFrame:3.0 callback:mockEngineFirstFrameCallback]);
750  id mockNavigationChannel = OCMClassMock([FlutterMethodChannel class]);
751  OCMStub([mockEngine navigationChannel]).andReturn(mockNavigationChannel);
752  id mockNavigationChannelCallback = [OCMArg invokeBlockWithArgs:@0, nil];
753  OCMStub([mockNavigationChannel invokeMethod:@"pushRouteInformation"
754  arguments:@{@"location" : urlString}
755  result:mockNavigationChannelCallback]);
756 
757  [mockEngine sendDeepLinkToFramework:url
758  completionHandler:^(BOOL success) {
759  XCTAssertFalse(success);
760  }];
761 }
762 @end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
flutter::Settings FLTDefaultSettingsForBundle(NSBundle *bundle, NSProcessInfo *processInfoOrNil)
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
flutter::Shell & shell()
const std::shared_ptr< IOSContext > & GetIosContext()
flutter::PlatformViewIOS * platformView()
FlutterEngine * spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:(/*nullable */NSString *entrypoint,[libraryURI]/*nullable */NSString *libraryURI,[initialRoute]/*nullable */NSString *initialRoute,[entrypointArgs]/*nullable */NSArray< NSString * > *entrypointArgs)
flutter::Shell & shell()
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
FlutterMethodChannel * navigationChannel
flutter::IOSRenderingAPI platformViewsRenderingAPI()
FlutterViewController * viewController
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
NSObject< FlutterTextureRegistry > * textureRegistry
NSMutableDictionary * pluginPublications
NSObject< FlutterBinaryMessenger > * binaryMessenger
void ensureSemanticsEnabled()
void waitForFirstFrame:callback:(NSTimeInterval timeout,[callback] void(^ callback)(BOOL didTimeout))
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
nullable NSObject< FlutterPluginRegistrar > * registrarForPlugin:(NSString *pluginKey)