blob: e0153800ed7a40a1acf49736b087a1ef13db422d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import <objc/runtime.h>
#import "flutter/common/settings.h"
#include "flutter/fml/synchronization/sync_switch.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
FLUTTER_ASSERT_ARC
@interface FlutterEngineSpy : FlutterEngine
@property(nonatomic) BOOL ensureSemanticsEnabledCalled;
@end
@implementation FlutterEngineSpy
- (void)ensureSemanticsEnabled {
_ensureSemanticsEnabledCalled = YES;
}
@end
@interface FlutterEngine () <FlutterTextInputDelegate>
@end
/// FlutterBinaryMessengerRelay used for testing that setting FlutterEngine.binaryMessenger to
/// the current instance doesn't trigger a use-after-free bug.
///
/// See: testSetBinaryMessengerToSameBinaryMessenger
@interface FakeBinaryMessengerRelay : FlutterBinaryMessengerRelay
@property(nonatomic, assign) BOOL failOnDealloc;
@end
@implementation FakeBinaryMessengerRelay
- (void)dealloc {
if (_failOnDealloc) {
XCTFail("FakeBinaryMessageRelay should not be deallocated");
}
}
@end
@interface FlutterEngineTest : XCTestCase
@end
@implementation FlutterEngineTest
- (void)setUp {
}
- (void)tearDown {
}
- (void)testCreate {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertNotNil(engine);
}
- (void)testInfoPlist {
// Check the embedded Flutter.framework Info.plist, not the linked dylib.
NSURL* flutterFrameworkURL =
[NSBundle.mainBundle.privateFrameworksURL URLByAppendingPathComponent:@"Flutter.framework"];
NSBundle* flutterBundle = [NSBundle bundleWithURL:flutterFrameworkURL];
XCTAssertEqualObjects(flutterBundle.bundleIdentifier, @"io.flutter.flutter");
NSDictionary<NSString*, id>* infoDictionary = flutterBundle.infoDictionary;
// OS version can have one, two, or three digits: "8", "8.0", "8.0.0"
NSError* regexError = NULL;
NSRegularExpression* osVersionRegex =
[NSRegularExpression regularExpressionWithPattern:@"((0|[1-9]\\d*)\\.)*(0|[1-9]\\d*)"
options:NSRegularExpressionCaseInsensitive
error:&regexError];
XCTAssertNil(regexError);
// Smoke test the test regex.
NSString* testString = @"9";
NSUInteger versionMatches =
[osVersionRegex numberOfMatchesInString:testString
options:NSMatchingAnchored
range:NSMakeRange(0, testString.length)];
XCTAssertEqual(versionMatches, 1UL);
testString = @"9.1";
versionMatches = [osVersionRegex numberOfMatchesInString:testString
options:NSMatchingAnchored
range:NSMakeRange(0, testString.length)];
XCTAssertEqual(versionMatches, 1UL);
testString = @"9.0.1";
versionMatches = [osVersionRegex numberOfMatchesInString:testString
options:NSMatchingAnchored
range:NSMakeRange(0, testString.length)];
XCTAssertEqual(versionMatches, 1UL);
testString = @".0.1";
versionMatches = [osVersionRegex numberOfMatchesInString:testString
options:NSMatchingAnchored
range:NSMakeRange(0, testString.length)];
XCTAssertEqual(versionMatches, 0UL);
// Test Info.plist values.
NSString* minimumOSVersion = infoDictionary[@"MinimumOSVersion"];
versionMatches = [osVersionRegex numberOfMatchesInString:minimumOSVersion
options:NSMatchingAnchored
range:NSMakeRange(0, minimumOSVersion.length)];
XCTAssertEqual(versionMatches, 1UL);
// SHA length is 40.
XCTAssertEqual(((NSString*)infoDictionary[@"FlutterEngine"]).length, 40UL);
// {clang_version} placeholder is 15 characters. The clang string version
// is longer than that, so check if the placeholder has been replaced, without
// actually checking a literal string, which could be different on various machines.
XCTAssertTrue(((NSString*)infoDictionary[@"ClangVersion"]).length > 15UL);
}
- (void)testDeallocated {
__weak FlutterEngine* weakEngine = nil;
@autoreleasepool {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
weakEngine = engine;
[engine run];
XCTAssertNotNil(weakEngine);
}
XCTAssertNil(weakEngine);
}
- (void)testSendMessageBeforeRun {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertNotNil(engine);
XCTAssertThrows([engine.binaryMessenger
sendOnChannel:@"foo"
message:[@"bar" dataUsingEncoding:NSUTF8StringEncoding]
binaryReply:nil]);
}
- (void)testSetMessageHandlerBeforeRun {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertNotNil(engine);
XCTAssertThrows([engine.binaryMessenger
setMessageHandlerOnChannel:@"foo"
binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply){
}]);
}
- (void)testNilSetMessageHandlerBeforeRun {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertNotNil(engine);
XCTAssertNoThrow([engine.binaryMessenger setMessageHandlerOnChannel:@"foo"
binaryMessageHandler:nil]);
}
- (void)testNotifyPluginOfDealloc {
id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
OCMStub([plugin detachFromEngineForRegistrar:[OCMArg any]]);
{
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"plugin"];
[registrar publish:plugin];
engine = nil;
}
OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]);
}
- (void)testSetBinaryMessengerToSameBinaryMessenger {
FakeBinaryMessengerRelay* fakeBinaryMessenger = [[FakeBinaryMessengerRelay alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine setBinaryMessenger:fakeBinaryMessenger];
// Verify that the setter doesn't free the old messenger before setting the new messenger.
fakeBinaryMessenger.failOnDealloc = YES;
[engine setBinaryMessenger:fakeBinaryMessenger];
// Don't fail when ARC releases the binary messenger.
fakeBinaryMessenger.failOnDealloc = NO;
}
- (void)testRunningInitialRouteSendsNavigationMessage {
id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine setBinaryMessenger:mockBinaryMessenger];
// Run with an initial route.
[engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
// Now check that an encoded method call has been made on the binary messenger to set the
// initial route to "test".
FlutterMethodCall* setInitialRouteMethodCall =
[FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
NSData* encodedSetInitialRouteMethod =
[[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
message:encodedSetInitialRouteMethod]);
}
- (void)testInitialRouteSettingsSendsNavigationMessage {
id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
auto settings = FLTDefaultSettingsForBundle();
settings.route = "test";
FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
[engine setBinaryMessenger:mockBinaryMessenger];
[engine run];
// Now check that an encoded method call has been made on the binary messenger to set the
// initial route to "test".
FlutterMethodCall* setInitialRouteMethodCall =
[FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
NSData* encodedSetInitialRouteMethod =
[[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
message:encodedSetInitialRouteMethod]);
}
- (void)testPlatformViewsControllerRenderingMetalBackend {
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine run];
flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kMetal);
}
- (void)testPlatformViewsControllerRenderingSoftware {
auto settings = FLTDefaultSettingsForBundle();
settings.enable_software_rendering = true;
FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
[engine run];
flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kSoftware);
}
- (void)testWaitForFirstFrameTimeout {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
[engine run];
XCTestExpectation* timeoutFirstFrame = [self expectationWithDescription:@"timeoutFirstFrame"];
[engine waitForFirstFrame:0.1
callback:^(BOOL didTimeout) {
if (timeoutFirstFrame) {
[timeoutFirstFrame fulfill];
}
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testSpawn {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
[engine run];
FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
libraryURI:nil
initialRoute:nil
entrypointArgs:nil];
XCTAssertNotNil(spawn);
}
- (void)testDeallocNotification {
XCTestExpectation* deallocNotification = [self expectationWithDescription:@"deallocNotification"];
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
id<NSObject> observer;
@autoreleasepool {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
observer = [center addObserverForName:kFlutterEngineWillDealloc
object:engine
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification* note) {
[deallocNotification fulfill];
}];
}
[self waitForExpectationsWithTimeout:1 handler:nil];
[center removeObserver:observer];
}
- (void)testSetHandlerAfterRun {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];
dispatch_async(dispatch_get_main_queue(), ^{
NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"foo"];
fml::AutoResetWaitableEvent latch;
[engine run];
flutter::Shell& shell = engine.shell;
engine.shell.GetTaskRunners().GetUITaskRunner()->PostTask([&latch, &shell] {
flutter::Engine::Delegate& delegate = shell;
auto message = std::make_unique<flutter::PlatformMessage>("foo", nullptr);
delegate.OnEngineHandlePlatformMessage(std::move(message));
latch.Signal();
});
latch.Wait();
[registrar.messenger setMessageHandlerOnChannel:@"foo"
binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) {
[gotMessage fulfill];
}];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testThreadPrioritySetCorrectly {
XCTestExpectation* prioritiesSet = [self expectationWithDescription:@"prioritiesSet"];
prioritiesSet.expectedFulfillmentCount = 3;
IMP mockSetThreadPriority =
imp_implementationWithBlock(^(NSThread* thread, double threadPriority) {
if ([thread.name hasSuffix:@".ui"]) {
XCTAssertEqual(threadPriority, 1.0);
[prioritiesSet fulfill];
} else if ([thread.name hasSuffix:@".raster"]) {
XCTAssertEqual(threadPriority, 1.0);
[prioritiesSet fulfill];
} else if ([thread.name hasSuffix:@".io"]) {
XCTAssertEqual(threadPriority, 0.5);
[prioritiesSet fulfill];
}
});
Method method = class_getInstanceMethod([NSThread class], @selector(setThreadPriority:));
IMP originalSetThreadPriority = method_getImplementation(method);
method_setImplementation(method, mockSetThreadPriority);
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine run];
[self waitForExpectationsWithTimeout:1 handler:nil];
method_setImplementation(method, originalSetThreadPriority);
}
- (void)testCanEnableDisableEmbedderAPIThroughInfoPlist {
{
// Not enable embedder API by default
auto settings = FLTDefaultSettingsForBundle();
settings.enable_software_rendering = true;
FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertFalse(engine.enableEmbedderAPI);
}
{
// Enable embedder api
id mockMainBundle = OCMPartialMock([NSBundle mainBundle]);
OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"])
.andReturn(@"YES");
auto settings = FLTDefaultSettingsForBundle();
settings.enable_software_rendering = true;
FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
XCTAssertTrue(engine.enableEmbedderAPI);
}
}
- (void)testFlutterTextInputViewDidResignFirstResponderWillCallTextInputClientConnectionClosed {
id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine setBinaryMessenger:mockBinaryMessenger];
[engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
[engine flutterTextInputView:nil didResignFirstResponderWithTextInputClient:1];
FlutterMethodCall* methodCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInputClient.onConnectionClosed"
arguments:@[ @(1) ]];
NSData* encodedMethodCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:methodCall];
OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/textinput" message:encodedMethodCall]);
}
- (void)testFlutterEngineUpdatesDisplays {
FlutterEngine* engine = [[FlutterEngine alloc] init];
id mockEngine = OCMPartialMock(engine);
[engine run];
OCMVerify(times(1), [mockEngine updateDisplays]);
engine.viewController = nil;
OCMVerify(times(2), [mockEngine updateDisplays]);
}
- (void)testLifeCycleNotificationDidEnterBackground {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
[engine run];
NSNotification* sceneNotification =
[NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
object:nil
userInfo:nil];
NSNotification* applicationNotification =
[NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
object:nil
userInfo:nil];
id mockEngine = OCMPartialMock(engine);
[[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
#if APPLICATION_EXTENSION_API_ONLY
OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]);
#else
OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]);
#endif
XCTAssertTrue(engine.isGpuDisabled);
bool switch_value = false;
[engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
switch_value = false;
}));
XCTAssertTrue(switch_value);
}
- (void)testLifeCycleNotificationWillEnterForeground {
FlutterDartProject* project = [[FlutterDartProject alloc] init];
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
[engine run];
NSNotification* sceneNotification =
[NSNotification notificationWithName:UISceneWillEnterForegroundNotification
object:nil
userInfo:nil];
NSNotification* applicationNotification =
[NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
object:nil
userInfo:nil];
id mockEngine = OCMPartialMock(engine);
[[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
#if APPLICATION_EXTENSION_API_ONLY
OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]);
#else
OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]);
#endif
XCTAssertFalse(engine.isGpuDisabled);
bool switch_value = true;
[engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
switch_value = false;
}));
XCTAssertFalse(switch_value);
}
- (void)testSpawnsShareGpuContext {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
[engine run];
FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
libraryURI:nil
initialRoute:nil
entrypointArgs:nil];
XCTAssertNotNil(spawn);
XCTAssertTrue([engine iosPlatformView] != nullptr);
XCTAssertTrue([spawn iosPlatformView] != nullptr);
std::shared_ptr<flutter::IOSContext> engine_context = [engine iosPlatformView]->GetIosContext();
std::shared_ptr<flutter::IOSContext> spawn_context = [spawn iosPlatformView]->GetIosContext();
XCTAssertEqual(engine_context, spawn_context);
// If this assert fails it means we may be using the software. For software rendering, this is
// expected to be nullptr.
XCTAssertTrue(engine_context->GetMainContext() != nullptr);
XCTAssertEqual(engine_context->GetMainContext(), spawn_context->GetMainContext());
}
- (void)testEnableSemanticsWhenFlutterViewAccessibilityDidCall {
FlutterEngineSpy* engine = [[FlutterEngineSpy alloc] initWithName:@"foobar"];
engine.ensureSemanticsEnabledCalled = NO;
[engine flutterViewAccessibilityDidCall];
XCTAssertTrue(engine.ensureSemanticsEnabledCalled);
}
@end