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