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