| // 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 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:args/args.dart'; | |
| import 'package:flutter_devicelab/framework/framework.dart'; | |
| import 'package:flutter_devicelab/framework/host_agent.dart'; | |
| import 'package:flutter_devicelab/framework/ios.dart'; | |
| import 'package:flutter_devicelab/framework/task_result.dart'; | |
| import 'package:flutter_devicelab/framework/utils.dart'; | |
| import 'package:path/path.dart' as path; | |
| /// This test creates a Flutter module, Flutter plugin, and native iOS app. | |
| /// It adds the plugin as a dependency and embeds the Flutter module into the native iOS app. | |
| /// Additional scenarios can be added in [Scenarios.scenarios]. | |
| /// | |
| /// To run this test locally, follow instructions in dev/devicelab/README.md. | |
| /// The `--local-engine` and `--local-engine-host` flags can be used to run with a local engine. | |
| /// | |
| /// `--task-args destination=[/path/to/copy/destination]` can be used to override the destination | |
| /// of the generated apps/plugins. | |
| /// | |
| /// e.g. `../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t module_uiscene_test_ios --local-engine ios_debug_sim_unopt_arm64 --local-engine-host host_debug --task-args destination=/path/to/copy/destination` | |
| Future<void> main(List<String> args) async { | |
| const kDestination = 'destination'; | |
| const kTestName = 'name'; | |
| const kXcodeProjecType = 'type'; | |
| final argParser = ArgParser() | |
| ..addOption(kDestination) | |
| ..addOption(kTestName) | |
| ..addOption(kXcodeProjecType); | |
| await task(() async { | |
| final ArgResults argResults = argParser.parse(args); | |
| final String? destination = argResults.option(kDestination); | |
| final Directory destinationDir; | |
| var destinationOverride = false; | |
| if (destination != null) { | |
| destinationOverride = true; | |
| destinationDir = Directory(destination); | |
| if (destinationDir.existsSync()) { | |
| destinationDir.deleteSync(recursive: true); | |
| } | |
| destinationDir.createSync(recursive: true); | |
| } else { | |
| destinationDir = Directory.systemTemp.createTempSync('flutter_module_test.'); | |
| } | |
| final String? testName = argResults.option(kTestName); | |
| XcodeProjectType? projectType; | |
| final String? projectTypeName = argResults.option(kXcodeProjecType); | |
| if (projectTypeName?.toLowerCase() == 'swiftui') { | |
| projectType = XcodeProjectType.SwiftUI; | |
| } else if (projectTypeName?.toLowerCase() == 'uikit-swift') { | |
| projectType = XcodeProjectType.UIKitSwift; | |
| } | |
| List<XcodeProjectType> projectTypesToTest = XcodeProjectType.values; | |
| if (projectType != null) { | |
| projectTypesToTest = <XcodeProjectType>[projectType]; | |
| } | |
| String? simulatorDeviceId; | |
| final templatesDir = Directory( | |
| path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_add2app_uiscene'), | |
| ); | |
| try { | |
| final Directory appDir = await _createFlutterModuleApp( | |
| destinationDir: destinationDir, | |
| templatesDir: templatesDir, | |
| ); | |
| final Directory pluginDir = await _createFlutterPlugin( | |
| destinationDir: destinationDir, | |
| templatesDir: templatesDir, | |
| ); | |
| var testCount = 0; | |
| var testFailedCount = 0; | |
| await testWithNewIOSSimulator( | |
| 'TestAdd2AppSim', | |
| deviceTypeId: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-3rd-generation', | |
| (String deviceId) async { | |
| for (final xcodeProjectType in projectTypesToTest) { | |
| final (String xcodeProjectName, Directory xcodeProjectDir) = await _createNativeApp( | |
| destinationDir: destinationDir, | |
| templatesDir: templatesDir, | |
| xcodeProjectType: xcodeProjectType, | |
| ); | |
| simulatorDeviceId = deviceId; | |
| final scenarios = Scenarios(); | |
| final Map<String, Map<String, String>> scenariosMap = scenarios.scenarios( | |
| xcodeProjectType, | |
| ); | |
| for (final String scenarioName in scenariosMap.keys) { | |
| if (testName != null && scenarioName != testName) { | |
| continue; | |
| } | |
| final List<FileReplacements> replacements = FileReplacements.fromScenario( | |
| scenariosMap[scenarioName]!, | |
| templatesDir: templatesDir, | |
| xcodeProjectDir: xcodeProjectDir, | |
| pluginDir: pluginDir, | |
| appDir: appDir, | |
| ); | |
| for (final replacement in replacements) { | |
| replacement.replace(); | |
| } | |
| section('Test Scenario $scenarioName'); | |
| await _installPlugins(appDir: appDir, xcodeProjectDir: xcodeProjectDir); | |
| final int result = await _testNativeApp( | |
| deviceId: simulatorDeviceId!, | |
| scenarioName: scenarioName, | |
| templatesDir: templatesDir, | |
| xcodeProjectDir: xcodeProjectDir, | |
| xcodeProjectName: xcodeProjectName, | |
| ); | |
| testCount++; | |
| if (result != 0) { | |
| testFailedCount++; | |
| } | |
| // Reset files to original between scenarios unless we're targetting a specific test. | |
| if (testName == null) { | |
| for (final replacement in replacements) { | |
| replacement.reset(); | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| ); | |
| if (testFailedCount > 0) { | |
| return TaskResult.failure( | |
| '$testFailedCount out of $testCount native tests failed. Search the logs for "** TEST FAILED **"', | |
| ); | |
| } | |
| return TaskResult.success(null); | |
| } catch (e, stackTrace) { | |
| print(e); | |
| print('Task exception stack trace:\n$stackTrace'); | |
| return TaskResult.failure(e.toString()); | |
| } finally { | |
| unawaited(removeIOSSimulator(simulatorDeviceId)); | |
| if (!destinationOverride) { | |
| rmTree(destinationDir); | |
| } | |
| } | |
| }); | |
| } | |
| Future<Directory> _createFlutterModuleApp({ | |
| required Directory templatesDir, | |
| required Directory destinationDir, | |
| }) async { | |
| section('Create Flutter Module'); | |
| const moduleName = 'my_module'; | |
| await flutter( | |
| 'create', | |
| options: <String>['--org', 'dev.flutter.devicelab', '--template=module', moduleName], | |
| workingDirectory: destinationDir.path, | |
| ); | |
| return Directory(path.join(destinationDir.path, moduleName)); | |
| } | |
| Future<Directory> _createFlutterPlugin({ | |
| required Directory templatesDir, | |
| required Directory destinationDir, | |
| }) async { | |
| section('Create Flutter Plugin'); | |
| const pluginName = 'my_plugin'; | |
| await flutter( | |
| 'create', | |
| options: <String>[ | |
| '--org', | |
| 'dev.flutter.devicelab', | |
| '--template=plugin', | |
| '--platform=ios', | |
| pluginName, | |
| ], | |
| workingDirectory: destinationDir.path, | |
| ); | |
| return Directory(path.join(destinationDir.path, pluginName)); | |
| } | |
| Future<(String, Directory)> _createNativeApp({ | |
| required Directory templatesDir, | |
| required Directory destinationDir, | |
| required XcodeProjectType xcodeProjectType, | |
| }) async { | |
| section('Create Xcode Project'); | |
| final String xcodeProjectName; | |
| switch (xcodeProjectType) { | |
| case XcodeProjectType.UIKitSwift: | |
| xcodeProjectName = 'NativeUIKitSwiftExperiment'; | |
| case XcodeProjectType.SwiftUI: | |
| xcodeProjectName = 'NativeSwiftUIExperiment'; | |
| } | |
| // Copy Xcode project | |
| final xcodeProjectDir = Directory(path.join(destinationDir.path, xcodeProjectName)); | |
| xcodeProjectDir.createSync(recursive: true); | |
| final xcodeProjectTemplate = Directory(path.join(templatesDir.path, xcodeProjectName)); | |
| recursiveCopy(xcodeProjectTemplate, xcodeProjectDir); | |
| return (xcodeProjectName, xcodeProjectDir); | |
| } | |
| Future<void> _installPlugins({ | |
| required Directory appDir, | |
| required Directory xcodeProjectDir, | |
| }) async { | |
| // Poke the pubspec to reset the fingerprinter to ensure the module is re-generated. | |
| // See [_regenerateModuleFromTemplateIfNeeded] in packages/flutter_tools/lib/src/xcode_project.dart. | |
| final pubspec = File(path.join(appDir.path, 'pubspec.yaml')); | |
| pubspec.writeAsStringSync(pubspec.readAsStringSync()); | |
| await flutter( | |
| 'build', | |
| options: <String>['ios', '--config-only', '-v'], | |
| workingDirectory: appDir.path, | |
| ); | |
| await flutter('pub', options: <String>['get'], workingDirectory: appDir.path); | |
| await eval( | |
| 'pod', | |
| <String>['install', '--verbose'], | |
| environment: <String, String>{ | |
| // See https://github.com/flutter/flutter/issues/10873. | |
| // CocoaPods analytics adds a lot of latency. | |
| 'COCOAPODS_DISABLE_STATS': 'true', | |
| 'LANG': 'en_US.UTF-8', | |
| }, | |
| workingDirectory: xcodeProjectDir.path, | |
| ); | |
| } | |
| class FileReplacements { | |
| FileReplacements(this.templatePath, this.destinationPath); | |
| final String templatePath; | |
| final String destinationPath; | |
| String? originalContent; | |
| static List<FileReplacements> fromScenario( | |
| Map<String, String> replacementMap, { | |
| required Directory templatesDir, | |
| required Directory xcodeProjectDir, | |
| required Directory pluginDir, | |
| required Directory appDir, | |
| }) { | |
| final replacements = <FileReplacements>[]; | |
| for (final String source in replacementMap.keys) { | |
| final String destination = replacementMap[source]!; | |
| final String sourcePath = source.replaceFirst(r'$TEMPLATE_DIR', templatesDir.path); | |
| final String destinationPath = destination | |
| .replaceFirst(r'$XCODE_PROJ_DIR', xcodeProjectDir.path) | |
| .replaceFirst(r'$PLUGIN_DIR', pluginDir.path) | |
| .replaceFirst(r'$APP_DIR', appDir.path); | |
| replacements.add(FileReplacements(sourcePath, destinationPath)); | |
| } | |
| return replacements; | |
| } | |
| void replace() { | |
| final templateFile = File(templatePath); | |
| final destinationFile = File(destinationPath); | |
| if (!destinationFile.existsSync()) { | |
| File(destinationPath).createSync(recursive: true); | |
| } else { | |
| originalContent = destinationFile.readAsStringSync(); | |
| } | |
| templateFile.copySync(destinationPath); | |
| } | |
| void reset() { | |
| final destinationFile = File(destinationPath); | |
| if (originalContent != null) { | |
| destinationFile.writeAsStringSync(originalContent!); | |
| } else { | |
| if (destinationFile.existsSync()) { | |
| destinationFile.deleteSync(recursive: true); | |
| } | |
| } | |
| } | |
| } | |
| Future<int> _testNativeApp({ | |
| required Directory templatesDir, | |
| required String scenarioName, | |
| required Directory xcodeProjectDir, | |
| required String deviceId, | |
| required String xcodeProjectName, | |
| }) async { | |
| final String resultBundleTemp = Directory.systemTemp | |
| .createTempSync('flutter_module_test_ios_xcresult.') | |
| .path; | |
| final String resultBundlePath = path.join(resultBundleTemp, 'result'); | |
| final int testResultExit = await exec( | |
| 'xcodebuild', | |
| <String>[ | |
| '-workspace', | |
| '$xcodeProjectName.xcworkspace', | |
| '-scheme', | |
| xcodeProjectName, | |
| '-configuration', | |
| 'Debug', | |
| '-destination', | |
| 'id=$deviceId', | |
| '-resultBundlePath', | |
| resultBundlePath, | |
| 'test', | |
| '-parallel-testing-enabled', | |
| 'NO', | |
| 'COMPILER_INDEX_STORE_ENABLE=NO', | |
| ], | |
| workingDirectory: xcodeProjectDir.path, | |
| canFail: true, | |
| ); | |
| if (testResultExit != 0) { | |
| await _uploadTestResults(scenarioName: scenarioName, resultBundlePath: resultBundleTemp); | |
| } | |
| return testResultExit; | |
| } | |
| Future<void> _uploadTestResults({ | |
| required String scenarioName, | |
| required String resultBundlePath, | |
| }) async { | |
| final Directory? dumpDirectory = hostAgent.dumpDirectory; | |
| if (dumpDirectory != null) { | |
| // Zip the test results to the artifacts directory for upload. | |
| final zipName = | |
| 'module_uiscene_test_ios-$scenarioName-${DateTime.now().toLocal().toIso8601String()}.zip'; | |
| await inDirectory(resultBundlePath, () { | |
| final String zipPath = path.join(dumpDirectory.path, zipName); | |
| return exec( | |
| 'zip', | |
| <String>['-r', '-9', '-q', zipPath, 'result.xcresult'], | |
| canFail: true, // Best effort to get the logs. | |
| ); | |
| }); | |
| } | |
| } | |
| enum XcodeProjectType { UIKitSwift, SwiftUI } | |
| class Scenarios { | |
| Scenarios(); | |
| Map<String, Map<String, String>> scenarios(XcodeProjectType projectType) { | |
| switch (projectType) { | |
| case XcodeProjectType.UIKitSwift: | |
| return uiKitSwiftScenarios; | |
| case XcodeProjectType.SwiftUI: | |
| return swiftUIScenarios; | |
| } | |
| } | |
| /// A map of scenario names to a map of file replacements. | |
| /// | |
| /// Each scenario is a different configuration for testing the Flutter module | |
| /// in a native iOS app. The file replacements are used to set up the | |
| /// specific configuration for each scenario. | |
| late Map<String, Map<String, String>> uiKitSwiftScenarios = <String, Map<String, String>>{ | |
| ...basicLifecycleScenarios, | |
| ...stateRestorationScenarios, | |
| ...implicitEngineDelegateScenarios, | |
| ...multiSceneScenarios, | |
| }; | |
| late Map<String, Map<String, String>> swiftUIScenarios = <String, Map<String, String>>{ | |
| 'SwiftUI-FlutterSceneDelegate': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperiment-Info.plist', | |
| r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperimentApp.swift', | |
| r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/ContentView.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperimentUITests/NativeSwiftUIExperimentUITests.swift', | |
| }, | |
| 'SwiftUI-FlutterSceneLifeCycleProvider': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperiment-Info.plist', | |
| r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperimentApp.swift', | |
| r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/ContentView.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeSwiftUIExperimentUITests/NativeSwiftUIExperimentUITests.swift', | |
| }, | |
| }; | |
| late Map<String, Map<String, String>> basicLifecycleScenarios = <String, Map<String, String>>{ | |
| // When both the app and the plugin have migrated to scenes, we expect scene events. | |
| 'AppMigrated-FlutterSceneDelegate-PluginMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When the app has migrated but the plugin hasn't, we expect application events to be used as | |
| // a fallback. | |
| 'AppMigrated-FlutterSceneDelegate-PluginNotMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppMigrated.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When both the app and the plugin have migrated to scenes, we expect scene events. | |
| 'AppMigrated-FlutterSceneLifeCycleProvider-PluginMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneLifeCycleProvider.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When the app has migrated but the plugin hasn't, we expect application events to be used as | |
| // a fallback. | |
| 'AppMigrated-FlutterSceneLifeCycleProvider-PluginNotMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneLifeCycleProvider.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppMigrated.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When the app has not migrated, but the plugin supports both, we expect application events. | |
| 'AppNotMigrated-FlutterSceneDelegate-PluginMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-unmigrated.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppNotMigrated.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When the app and plugin have not migrated, we expect application events. | |
| 'AppNotMigrated-FlutterSceneDelegate-PluginNotMigrated': <String, String>{ | |
| ...sharedLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-unmigrated.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppNotMigrated.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| }; | |
| late Map<String, Map<String, String>> multiSceneScenarios = <String, Map<String, String>>{ | |
| // When multi scene is enabled and the rootViewController is a FlutterViewController, we | |
| // expect all scene events without manual registration. | |
| 'MultiSceneEnabled-FlutterSceneDelegate-RootViewController': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-Storyboard.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When multi scene is enabled and the ViewController is created programatically with a | |
| // manually registered FlutterEngine, we expect all scene events. | |
| 'MultiSceneEnabled-FlutterSceneDelegate-ManualRegistration-NoStoryboard': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-NoStoryboard.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate-MultiScene-NoStoryboard.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromSceneDelegate-NoStoryboard.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When multi scene is enabled and the ViewController is created via Storyboard with a | |
| // manually registered FlutterEngine, we expect all scene events. | |
| 'MultiSceneEnabled-FlutterSceneDelegate-ManualRegistration-Storyboard': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-Storyboard.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate-MultiScene-Storyboard.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromSceneDelegate-Storyboard.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| }; | |
| late Map<String, Map<String, String>> stateRestorationScenarios = <String, Map<String, String>>{ | |
| // State restoration work both when migrated and when not. | |
| 'AppMigrated-StateRestoration': <String, String>{...sharedStateRestorationFiles}, | |
| 'AppNotMigrated-StateRestoration': <String, String>{ | |
| ...sharedStateRestorationFiles, | |
| r'$TEMPLATE_DIR/native/Info-unmigrated.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| }, | |
| }; | |
| late Map<String, Map<String, String>> | |
| implicitEngineDelegateScenarios = <String, Map<String, String>>{ | |
| // When using an implicit FlutterEngine created by the storyboard, we expect plugins to | |
| // receive application launch events and scene events. | |
| 'FlutterImplicitEngineDelegate-AppMigrated-StoryboardFlutterViewController': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents-ApplicationLaunchEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When registering plugins with the AppDelegate's self (and therefore the FlutterLaunchEngine) | |
| // alongside the FlutterImplicitEngineDelegate, we expect application events starting where | |
| // registration occurs, such as `application:didFinishingLaunchingWithOptions`. | |
| 'FlutterImplicitEngineDelegateWithLaunchEngine-AppMigrated-StoryboardFlutterViewController': | |
| <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegateWithLaunchEngine.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When the app has not migrated to scenes, storyboard is instantiated earlier in the lifecycle. | |
| // So when using an implicit FlutterEngine created by the storyboard, we expect plugins to | |
| // receive all application events. | |
| 'FlutterImplicitEngineDelegate-AppNotMigrated-StoryboardFlutterViewController': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/Info-unmigrated.plist': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', | |
| r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-FlutterImplicitEngineDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| // When using an implicit FlutterEngine, created by the FlutterViewController in another | |
| // ViewController, we expect plugins to be registered after the FlutterViewController is | |
| // created, which results in the `application:didFinishLaunchingWithOptions:` and | |
| // `scene:willConnectToSession:options:` events being missed. This is not a expected use case | |
| // but it could be utilized. | |
| 'FlutterImplicitEngineDelegate-AppMigrated-ImplicitFlutterEngine': <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': | |
| r'$PLUGIN_DIR/ios/my_plugin/Sources/my_plugin/MyPlugin.swift', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/ViewController-ImplicitFlutterEngine.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', | |
| r'$TEMPLATE_DIR/native/UITests-SceneEventsNoConnect-NoApplicationEvents.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }, | |
| }; | |
| late Map<String, String> sharedLifecycleFiles = <String, String>{ | |
| ...sharedAppLifecycleFiles, | |
| ...sharedPluginLifecycleFiles, | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate-FlutterEngine.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromAppDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', | |
| }; | |
| late Map<String, String> sharedAppLifecycleFiles = <String, String>{ | |
| r'$TEMPLATE_DIR/flutterapp/lib/main-LifeCycleTest': r'$APP_DIR/lib/main.dart', | |
| r'$TEMPLATE_DIR/flutterapp/pubspec-LifeCycleTest.yaml': r'$APP_DIR/pubspec.yaml', | |
| }; | |
| late Map<String, String> sharedPluginLifecycleFiles = <String, String>{ | |
| r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin': r'$PLUGIN_DIR/lib/my_plugin.dart', | |
| r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin_method_channel': | |
| r'$PLUGIN_DIR/lib/my_plugin_method_channel.dart', | |
| r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin_platform_interface': | |
| r'$PLUGIN_DIR/lib/my_plugin_platform_interface.dart', | |
| }; | |
| late Map<String, String> sharedStateRestorationFiles = <String, String>{ | |
| r'$TEMPLATE_DIR/flutterapp/lib/main-StateRestorationTest': r'$APP_DIR/lib/main.dart', | |
| r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate-FlutterEngine.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', | |
| r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', | |
| r'$TEMPLATE_DIR/native/Main-FlutterViewController-RestorationId.storyboard': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', | |
| r'$TEMPLATE_DIR/native/UITests-StateRestoration.swift': | |
| r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', | |
| }; | |
| } |