| 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"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
14 #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h"
16 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h"
17 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
18
19 FLUTTER_ASSERT_ARC
20
21 @interface FlutterEngine () <FlutterTextInputDelegate>
22
23 @end
24
25 /// FlutterBinaryMessengerRelay used for testing that setting FlutterEngine.binaryMessenger to
26 /// the current instance doesn't trigger a use-after-free bug.
27 ///
28 /// See: testSetBinaryMessengerToSameBinaryMessenger
29 @interface FakeBinaryMessengerRelay : FlutterBinaryMessengerRelay
30 @property(nonatomic, assign) BOOL failOnDealloc;
31 @end
32
33 @implementation FakeBinaryMessengerRelay
34 - (void)dealloc {
35 if (_failOnDealloc) {
36 XCTFail("FakeBinaryMessageRelay should not be deallocated");
37 }
38 }
39 @end
40
41 @interface FlutterEngineTest : XCTestCase
42 @end
43
44 @implementation FlutterEngineTest
45
46 - (void)setUp {
47 }
48
49 - (void)tearDown {
50 }
51
52 - (void)testCreate {
53 FlutterDartProject* project = [[FlutterDartProject alloc] init];
54 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
55 XCTAssertNotNil(engine);
56 }
57
58 - (void)testInfoPlist {
59 // Check the embedded Flutter.framework Info.plist, not the linked dylib.
60 NSURL* flutterFrameworkURL =
61 [NSBundle.mainBundle.privateFrameworksURL URLByAppendingPathComponent:@"Flutter.framework"];
62 NSBundle* flutterBundle = [NSBundle bundleWithURL:flutterFrameworkURL];
63 XCTAssertEqualObjects(flutterBundle.bundleIdentifier, @"io.flutter.flutter");
64
65 NSDictionary<NSString*, id>* infoDictionary = flutterBundle.infoDictionary;
66
67 // OS version can have one, two, or three digits: "8", "8.0", "8.0.0"
68 NSError* regexError = NULL;
69 NSRegularExpression* osVersionRegex =
70 [NSRegularExpression regularExpressionWithPattern:@"((0|[1-9]\\d*)\\.)*(0|[1-9]\\d*)"
71 options:NSRegularExpressionCaseInsensitive
72 error:&regexError];
73 XCTAssertNil(regexError);
74
75 // Smoke test the test regex.
76 NSString* testString = @"9";
77 NSUInteger versionMatches =
78 [osVersionRegex numberOfMatchesInString:testString
79 options:NSMatchingAnchored
80 range:NSMakeRange(0, testString.length)];
81 XCTAssertEqual(versionMatches, 1UL);
82 testString = @"9.1";
83 versionMatches = [osVersionRegex numberOfMatchesInString:testString
84 options:NSMatchingAnchored
85 range:NSMakeRange(0, testString.length)];
86 XCTAssertEqual(versionMatches, 1UL);
87 testString = @"9.0.1";
88 versionMatches = [osVersionRegex numberOfMatchesInString:testString
89 options:NSMatchingAnchored
90 range:NSMakeRange(0, testString.length)];
91 XCTAssertEqual(versionMatches, 1UL);
92 testString = @".0.1";
93 versionMatches = [osVersionRegex numberOfMatchesInString:testString
94 options:NSMatchingAnchored
95 range:NSMakeRange(0, testString.length)];
96 XCTAssertEqual(versionMatches, 0UL);
97
98 // Test Info.plist values.
99 NSString* minimumOSVersion = infoDictionary[@"MinimumOSVersion"];
100 versionMatches = [osVersionRegex numberOfMatchesInString:minimumOSVersion
101 options:NSMatchingAnchored
102 range:NSMakeRange(0, minimumOSVersion.length)];
103 XCTAssertEqual(versionMatches, 1UL);
104
105 // SHA length is 40.
106 XCTAssertEqual(((NSString*)infoDictionary[@"FlutterEngine"]).length, 40UL);
107
108 // {clang_version} placeholder is 15 characters. The clang string version
109 // is longer than that, so check if the placeholder has been replaced, without
110 // actually checking a literal string, which could be different on various machines.
111 XCTAssertTrue(((NSString*)infoDictionary[@"ClangVersion"]).length > 15UL);
112 }
113
114 - (void)testDeallocated {
115 __weak FlutterEngine* weakEngine = nil;
116 @autoreleasepool {
117 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
118 weakEngine = engine;
119 [engine run];
120 XCTAssertNotNil(weakEngine);
121 }
122 XCTAssertNil(weakEngine);
123 }
124
125 - (void)testSendMessageBeforeRun {
126 FlutterDartProject* project = [[FlutterDartProject alloc] init];
127 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
128 XCTAssertNotNil(engine);
129 XCTAssertThrows([engine.binaryMessenger
130 sendOnChannel:@"foo"
131 message:[@"bar" dataUsingEncoding:NSUTF8StringEncoding]
132 binaryReply:nil]);
133 }
134
135 - (void)testSetMessageHandlerBeforeRun {
136 FlutterDartProject* project = [[FlutterDartProject alloc] init];
137 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
138 XCTAssertNotNil(engine);
139 XCTAssertThrows([engine.binaryMessenger
140 setMessageHandlerOnChannel:@"foo"
141 binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply){
142
143 }]);
144 }
145
146 - (void)testNilSetMessageHandlerBeforeRun {
147 FlutterDartProject* project = [[FlutterDartProject alloc] init];
148 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
149 XCTAssertNotNil(engine);
150 XCTAssertNoThrow([engine.binaryMessenger setMessageHandlerOnChannel:@"foo"
151 binaryMessageHandler:nil]);
152 }
153
154 - (void)testNotifyPluginOfDealloc {
155 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
156 OCMStub([plugin detachFromEngineForRegistrar:[OCMArg any]]);
157 {
158 FlutterDartProject* project = [[FlutterDartProject alloc] init];
159 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
160 NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"plugin"];
161 [registrar publish:plugin];
162 engine = nil;
163 }
164 OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]);
165 }
166
167 - (void)testSetBinaryMessengerToSameBinaryMessenger {
168 FakeBinaryMessengerRelay* fakeBinaryMessenger = [[FakeBinaryMessengerRelay alloc] init];
169
170 FlutterEngine* engine = [[FlutterEngine alloc] init];
171 [engine setBinaryMessenger:fakeBinaryMessenger];
172
173 // Verify that the setter doesn't free the old messenger before setting the new messenger.
174 fakeBinaryMessenger.failOnDealloc = YES;
175 [engine setBinaryMessenger:fakeBinaryMessenger];
176
177 // Don't fail when ARC releases the binary messenger.
178 fakeBinaryMessenger.failOnDealloc = NO;
179 }
180
181 - (void)testRunningInitialRouteSendsNavigationMessage {
182 id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
183
184 FlutterEngine* engine = [[FlutterEngine alloc] init];
185 [engine setBinaryMessenger:mockBinaryMessenger];
186
187 // Run with an initial route.
188 [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
189
190 // Now check that an encoded method call has been made on the binary messenger to set the
191 // initial route to "test".
192 FlutterMethodCall* setInitialRouteMethodCall =
193 [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
194 NSData* encodedSetInitialRouteMethod =
195 [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
196 OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
197 message:encodedSetInitialRouteMethod]);
198 }
199
200 - (void)testInitialRouteSettingsSendsNavigationMessage {
201 id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
202
203 auto settings = FLTDefaultSettingsForBundle();
204 settings.route = "test";
205 FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
206 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
207 [engine setBinaryMessenger:mockBinaryMessenger];
208 [engine run];
209
210 // Now check that an encoded method call has been made on the binary messenger to set the
211 // initial route to "test".
212 FlutterMethodCall* setInitialRouteMethodCall =
213 [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
214 NSData* encodedSetInitialRouteMethod =
215 [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
216 OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
217 message:encodedSetInitialRouteMethod]);
218 }
219
220 - (void)testPlatformViewsControllerRenderingMetalBackend {
221 FlutterEngine* engine = [[FlutterEngine alloc] init];
222 [engine run];
223 flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
224
225 XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kMetal);
226 }
227
228 - (void)testPlatformViewsControllerRenderingSoftware {
229 auto settings = FLTDefaultSettingsForBundle();
230 settings.enable_software_rendering = true;
231 FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
232 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
233 [engine run];
234 flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
235
236 XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kSoftware);
237 }
238
239 - (void)testWaitForFirstFrameTimeout {
240 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
241 [engine run];
242 XCTestExpectation* timeoutFirstFrame = [self expectationWithDescription:@"timeoutFirstFrame"];
243 [engine waitForFirstFrame:0.1
244 callback:^(BOOL didTimeout) {
245 if (timeoutFirstFrame) {
246 [timeoutFirstFrame fulfill];
247 }
248 }];
249 [self waitForExpectationsWithTimeout:5 handler:nil];
250 }
251
252 - (void)testSpawn {
253 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
254 [engine run];
255 FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
256 libraryURI:nil
257 initialRoute:nil
258 entrypointArgs:nil];
259 XCTAssertNotNil(spawn);
260 }
261
262 - (void)testDeallocNotification {
263 XCTestExpectation* deallocNotification = [self expectationWithDescription:@"deallocNotification"];
264 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
265 id<NSObject> observer;
266 @autoreleasepool {
267 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
268 observer = [center addObserverForName:kFlutterEngineWillDealloc
269 object:engine
270 queue:[NSOperationQueue mainQueue]
271 usingBlock:^(NSNotification* note) {
272 [deallocNotification fulfill];
273 }];
274 }
275 [self waitForExpectationsWithTimeout:1 handler:nil];
276 [center removeObserver:observer];
277 }
278
279 - (void)testSetHandlerAfterRun {
280 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
281 XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];
282 dispatch_async(dispatch_get_main_queue(), ^{
283 NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"foo"];
284 fml::AutoResetWaitableEvent latch;
285 [engine run];
286 flutter::Shell& shell = engine.shell;
287 engine.shell.GetTaskRunners().GetUITaskRunner()->PostTask([&latch, &shell] {
288 flutter::Engine::Delegate& delegate = shell;
289 auto message = std::make_unique<flutter::PlatformMessage>("foo", nullptr);
290 delegate.OnEngineHandlePlatformMessage(std::move(message));
291 latch.Signal();
292 });
293 latch.Wait();
294 [registrar.messenger setMessageHandlerOnChannel:@"foo"
295 binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) {
296 [gotMessage fulfill];
297 }];
298 });
299 [self waitForExpectationsWithTimeout:1 handler:nil];
300 }
301
302 - (void)testThreadPrioritySetCorrectly {
303 XCTestExpectation* prioritiesSet = [self expectationWithDescription:@"prioritiesSet"];
304 prioritiesSet.expectedFulfillmentCount = 3;
305
306 IMP mockSetThreadPriority =
307 imp_implementationWithBlock(^(NSThread* thread, double threadPriority) {
308 if ([thread.name hasSuffix:@".ui"]) {
309 XCTAssertEqual(threadPriority, 1.0);
310 [prioritiesSet fulfill];
311 } else if ([thread.name hasSuffix:@".raster"]) {
312 XCTAssertEqual(threadPriority, 1.0);
313 [prioritiesSet fulfill];
314 } else if ([thread.name hasSuffix:@".io"]) {
315 XCTAssertEqual(threadPriority, 0.5);
316 [prioritiesSet fulfill];
317 }
318 });
319 Method method = class_getInstanceMethod([NSThread class], @selector(setThreadPriority:));
320 IMP originalSetThreadPriority = method_getImplementation(method);
321 method_setImplementation(method, mockSetThreadPriority);
322
323 FlutterEngine* engine = [[FlutterEngine alloc] init];
324 [engine run];
325 [self waitForExpectationsWithTimeout:1 handler:nil];
326
327 method_setImplementation(method, originalSetThreadPriority);
328 }
329
330 - (void)testCanEnableDisableEmbedderAPIThroughInfoPlist {
331 {
332 // Not enable embedder API by default
333 auto settings = FLTDefaultSettingsForBundle();
334 settings.enable_software_rendering = true;
335 FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
336 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
337 XCTAssertFalse(engine.enableEmbedderAPI);
338 }
339 {
340 // Enable embedder api
341 id mockMainBundle = OCMPartialMock([NSBundle mainBundle]);
342 OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"])
343 .andReturn(@"YES");
344 auto settings = FLTDefaultSettingsForBundle();
345 settings.enable_software_rendering = true;
346 FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
347 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
348 XCTAssertTrue(engine.enableEmbedderAPI);
349 }
350 }
351
352 - (void)testFlutterTextInputViewDidResignFirstResponderWillCallTextInputClientConnectionClosed {
353 id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
354 FlutterEngine* engine = [[FlutterEngine alloc] init];
355 [engine setBinaryMessenger:mockBinaryMessenger];
356 [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
357 [engine flutterTextInputView:nil didResignFirstResponderWithTextInputClient:1];
358 FlutterMethodCall* methodCall =
359 [FlutterMethodCall methodCallWithMethodName:@"TextInputClient.onConnectionClosed"
360 arguments:@[ @(1) ]];
361 NSData* encodedMethodCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:methodCall];
362 OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/textinput" message:encodedMethodCall]);
363 }
364
365 - (void)testFlutterEngineUpdatesDisplays {
366 FlutterEngine* engine = [[FlutterEngine alloc] init];
367 id mockEngine = OCMPartialMock(engine);
368
369 [engine run];
370 OCMVerify(times(1), [mockEngine updateDisplays]);
371 engine.viewController = nil;
372 OCMVerify(times(2), [mockEngine updateDisplays]);
373 }
374
375 - (void)testLifeCycleNotificationDidEnterBackground {
376 FlutterDartProject* project = [[FlutterDartProject alloc] init];
377 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
378 [engine run];
379 NSNotification* sceneNotification =
380 [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
381 object:nil
382 userInfo:nil];
383 NSNotification* applicationNotification =
384 [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
385 object:nil
386 userInfo:nil];
387 id mockEngine = OCMPartialMock(engine);
388 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
389 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
390 #if APPLICATION_EXTENSION_API_ONLY
391 OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]);
392 #else
393 OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]);
394 #endif
395 XCTAssertTrue(engine.isGpuDisabled);
396 bool switch_value = false;
397 [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
398 fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
399 switch_value = false;
400 }));
401 XCTAssertTrue(switch_value);
402 }
403
404 - (void)testLifeCycleNotificationWillEnterForeground {
405 FlutterDartProject* project = [[FlutterDartProject alloc] init];
406 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
407 [engine run];
408 NSNotification* sceneNotification =
409 [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
410 object:nil
411 userInfo:nil];
412 NSNotification* applicationNotification =
413 [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
414 object:nil
415 userInfo:nil];
416 id mockEngine = OCMPartialMock(engine);
417 [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
418 [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
419 #if APPLICATION_EXTENSION_API_ONLY
420 OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]);
421 #else
422 OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]);
423 #endif
424 XCTAssertFalse(engine.isGpuDisabled);
425 bool switch_value = true;
426 [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
427 fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
428 switch_value = false;
429 }));
430 XCTAssertFalse(switch_value);
431 }
432
433 @end
FLUTTER_ASSERT_ARC
#define FLUTTER_ASSERT_ARC
Definition: FlutterMacros.h:44
FlutterEngine
Definition: FlutterEngine.h:59
FlutterPlugin-p
Definition: FlutterPlugin.h:189
FlutterTextInputDelegate-p
Definition: FlutterTextInputDelegate.h:33
-[FlutterEngine waitForFirstFrame:callback:]
void waitForFirstFrame:callback:(NSTimeInterval timeout,[callback] void(^ callback)(BOOL didTimeout))
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
-[FlutterEngine platformViewsRenderingAPI]
flutter::IOSRenderingAPI platformViewsRenderingAPI()
FlutterEngine_Test.h
FlutterTextInputPlugin.h
FlutterMacros.h
-[FlutterEngine setBinaryMessenger:]
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
-[FlutterEngine shell]
flutter::Shell & shell()
FlutterBinaryMessengerRelay.h
FakeBinaryMessengerRelay::failOnDealloc
BOOL failOnDealloc
Definition: FlutterEngineTest.mm:30
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterEngineTest
Definition: FlutterEngineTest.mm:41
flutter::IOSRenderingAPI
IOSRenderingAPI
Definition: rendering_api_selection.h:14
-[FlutterPluginRegistry-p registrarForPlugin:]
nullable NSObject< FlutterPluginRegistrar > * registrarForPlugin:(NSString *pluginKey)
flutter::IOSRenderingAPI::kMetal
@ kMetal
FakeBinaryMessengerRelay
Definition: FlutterEngineTest.mm:29
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
FlutterDartProject_Internal.h
FlutterBinaryMessengerRelay
Definition: FlutterBinaryMessengerRelay.h:14
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:453
FLTDefaultSettingsForBundle
flutter::Settings FLTDefaultSettingsForBundle(NSBundle *bundle, NSProcessInfo *processInfoOrNil)
Definition: FlutterDartProject.mm:55
FlutterDartProject
Definition: FlutterDartProject.mm:262
-[FlutterEngine runWithEntrypoint:initialRoute:]
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
-[FlutterEngine spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:]
FlutterEngine * spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:(/*nullable */NSString *entrypoint,[libraryURI]/*nullable */NSString *libraryURI,[initialRoute]/*nullable */NSString *initialRoute,[entrypointArgs]/*nullable */NSArray< NSString * > *entrypointArgs)
-[FlutterEngine run]
BOOL run()
Definition: FlutterEngine.mm:938
flutter::IOSRenderingAPI::kSoftware
@ kSoftware
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)