// Copyright 2014 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 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/migrations/uiscene_migration.dart';
import '../src/common.dart';
import 'test_utils.dart';
void main() {
test(
'Auto migrate Swift app',
() async {
final Directory workingDirectory = fileSystem.systemTempDirectory.createTempSync(
'uiscene_migration.',
);
final String workingDirectoryPath = workingDirectory.path;
addTearDown(() async {
await _disableUISceneMigration(flutterBin, workingDirectoryPath);
ErrorHandlingFileSystem.deleteIfExists(workingDirectory, recursive: true);
});
await _enableUISceneMigration(flutterBin, workingDirectoryPath);
// Create app with scene templates
final String appDirectoryPath = await _createApp(flutterBin, workingDirectoryPath);
final File appDelegate = fileSystem.file(
fileSystem.path.join(appDirectoryPath, 'ios', 'Runner', 'AppDelegate.swift'),
);
final File infoPlist = fileSystem.file(
fileSystem.path.join(appDirectoryPath, 'ios', 'Runner', 'Info.plist'),
);
expect(appDelegate, exists);
expect(infoPlist, exists);
expect(appDelegate.readAsStringSync(), UISceneMigration.newSwiftAppDelegate);
expect(
infoPlist.readAsStringSync(),
_newInfoPlistTemplate('Uiscene Migration App', 'uiscene_migration_app', fromTemplate: true),
);
await _buildApp(flutterBin, appDirectoryPath);
// Replace with old template
final String oldInfoPlistTemplate = _oldInfoPlistTemplate(
'Uiscene Migration App',
'uiscene_migration_app',
);
appDelegate.writeAsStringSync(UISceneMigration.originalSwiftAppDelegateTemplates.first);
infoPlist.writeAsStringSync(oldInfoPlistTemplate);
// Make sure it migrates and builds
await _buildApp(flutterBin, appDirectoryPath);
expect(appDelegate.readAsStringSync(), UISceneMigration.newSwiftAppDelegate);
expect(
infoPlist.readAsStringSync(),
_newInfoPlistTemplate('Uiscene Migration App', 'uiscene_migration_app'),
);
// Turn off config
await _disableUISceneMigration(flutterBin, workingDirectoryPath);
// Replace with old template
appDelegate.writeAsStringSync(_oldSwiftAppDelegate);
infoPlist.writeAsStringSync(oldInfoPlistTemplate);
// Make sure it doesn't migrate
await _buildApp(flutterBin, appDirectoryPath);
expect(appDelegate.readAsStringSync(), _oldSwiftAppDelegate);
expect(infoPlist.readAsStringSync(), oldInfoPlistTemplate);
},
skip: !platform.isMacOS, // [intended] macOS builds only work on macos.
);
test(
'Auto migrate ObjC app',
() async {
final String appDirectoryPath = fileSystem.path.join(
getFlutterRoot(),
'dev',
'integration_tests',
'spell_check',
);
final File appDelegateHeader = fileSystem.file(
fileSystem.path.join(appDirectoryPath, 'ios', 'Runner', 'AppDelegate.h'),
);
final File appDelegateImpl = fileSystem.file(
fileSystem.path.join(appDirectoryPath, 'ios', 'Runner', 'AppDelegate.m'),
);
final File infoPlist = fileSystem.file(
fileSystem.path.join(appDirectoryPath, 'ios', 'Runner', 'Info.plist'),
);
expect(appDelegateHeader, exists);
expect(appDelegateImpl, exists);
expect(infoPlist, exists);
final String originalAppDelegateHeader = appDelegateHeader.readAsStringSync();
final String originalAppDelegateImpl = appDelegateImpl.readAsStringSync();
final String originalInfoPlist = infoPlist.readAsStringSync();
addTearDown(() async {
await _disableUISceneMigration(flutterBin, appDirectoryPath);
appDelegateHeader.writeAsStringSync(originalAppDelegateHeader);
appDelegateImpl.writeAsStringSync(originalAppDelegateImpl);
infoPlist.writeAsStringSync(originalInfoPlist);
});
await _enableUISceneMigration(flutterBin, appDirectoryPath);
// Remove license so will match
appDelegateHeader.writeAsStringSync(
originalAppDelegateHeader.replaceAll(flutter2014License, ''),
);
appDelegateImpl.writeAsStringSync(originalAppDelegateImpl.replaceAll(flutter2014License, ''));
// Make sure it migrates and builds
await _buildApp(flutterBin, appDirectoryPath);
expect(appDelegateHeader.readAsStringSync(), UISceneMigration.newObjCAppDelegateHeader);
expect(appDelegateImpl.readAsStringSync(), UISceneMigration.newObjCAppDelegateImplementation);
expect(infoPlist.readAsStringSync(), _newInfoPlistTemplate('Spell Check', 'spell_check'));
// Turn off config
await _disableUISceneMigration(flutterBin, appDirectoryPath);
// Replace with old template
appDelegateHeader.writeAsStringSync(originalAppDelegateHeader);
appDelegateImpl.writeAsStringSync(originalAppDelegateImpl);
infoPlist.writeAsStringSync(originalInfoPlist);
// Make sure it doesn't migrate
await _buildApp(flutterBin, appDirectoryPath);
expect(appDelegateHeader.readAsStringSync(), originalAppDelegateHeader);
expect(appDelegateImpl.readAsStringSync(), originalAppDelegateImpl);
expect(infoPlist.readAsStringSync(), originalInfoPlist);
},
skip: !platform.isMacOS, // [intended] macOS builds only work on macos.
);
}
const flutter2014License = '''
// Copyright 2014 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.
''';
String _oldInfoPlistTemplate(String titleCaseProjectName, String projectName) {
return '''
CFBundleDevelopmentRegion
\$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
$titleCaseProjectName
CFBundleExecutable
\$(EXECUTABLE_NAME)
CFBundleIdentifier
\$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$projectName
CFBundlePackageType
APPL
CFBundleShortVersionString
\$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
CFBundleVersion
\$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
CADisableMinimumFrameDurationOnPhone
UIApplicationSupportsIndirectInputEvents
''';
}
const _oldSwiftAppDelegate = r'''
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
''';
String _newInfoPlistTemplate(
String titleCaseProjectName,
String projectName, {
bool fromTemplate = false,
}) {
return '''
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
\$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
$titleCaseProjectName
CFBundleExecutable
\$(EXECUTABLE_NAME)
CFBundleIdentifier
\$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$projectName
CFBundlePackageType
APPL
CFBundleShortVersionString
\$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
CFBundleVersion
\$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
UISceneConfigurations
UIWindowSceneSessionRoleApplication
UISceneClassName
UIWindowScene
UISceneConfigurationName
flutter
UISceneDelegateClassName
${fromTemplate ? r'$(PRODUCT_MODULE_NAME).SceneDelegate' : 'FlutterSceneDelegate'}
UISceneStoryboardFile
Main
UIApplicationSupportsIndirectInputEvents
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
''';
}
Future _createApp(
String flutterBin,
String workingDirectory, {
List options = const [],
}) async {
const appName = 'uiscene_migration_app';
final ProcessResult result = await processManager.run([
flutterBin,
...getLocalEngineArguments(),
'create',
'--org',
'io.flutter.devicelab',
'--platforms=ios',
...options,
appName,
], workingDirectory: workingDirectory);
expect(
result.exitCode,
0,
reason:
'Failed to create app: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
);
return fileSystem.path.join(workingDirectory, appName);
}
Future _buildApp(
String flutterBin,
String appDirectory, {
List options = const [],
}) async {
final ProcessResult result = await processManager.run([
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
...options,
], workingDirectory: appDirectory);
expect(
result.exitCode,
0,
reason:
'Failed to build app: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
);
}
Future _enableUISceneMigration(String flutterBin, String workingDirectory) async {
final ProcessResult result = await processManager.run([
flutterBin,
...getLocalEngineArguments(),
'config',
'--enable-uiscene-migration',
'-v',
], workingDirectory: workingDirectory);
expect(
result.exitCode,
0,
reason:
'Failed to enable Swift Package Manager: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
);
}
Future _disableUISceneMigration(String flutterBin, String workingDirectory) async {
final ProcessResult result = await processManager.run([
flutterBin,
...getLocalEngineArguments(),
'config',
'--no-enable-uiscene-migration',
'-v',
], workingDirectory: workingDirectory);
expect(
result.exitCode,
0,
reason:
'Failed to enable Swift Package Manager: \n'
'stdout: \n${result.stdout}\n'
'stderr: \n${result.stderr}\n',
);
}