Unverified Commit be81e9ea authored by hellohuanlin's avatar hellohuanlin Committed by GitHub

[tools]build ipa validate launch image using template files (#116242)

* [tools]build ipa validate launch image using template files

* reuse more code by sharing the same file key

* fix space
parent 0234b18f
...@@ -46,10 +46,13 @@ Future<void> main() async { ...@@ -46,10 +46,13 @@ Future<void> main() async {
throw TaskResult.failure('Must validate incorrect app icon image size.'); throw TaskResult.failure('Must validate incorrect app icon image size.');
} }
// The project is still using Flutter template icon. // The project is still using Flutter template icon and launch image.
if (!output.contains('Warning: App icon is set to the default placeholder icon. Replace with unique icons.')) { if (!output.contains('Warning: App icon is set to the default placeholder icon. Replace with unique icons.')) {
throw TaskResult.failure('Must validate template app icon.'); throw TaskResult.failure('Must validate template app icon.');
} }
if (!output.contains('Warning: Launch image is set to the default placeholder. Replace with unique launch images.')) {
throw TaskResult.failure('Must validate template launch image.');
}
}); });
final String archivePath = path.join( final String archivePath = path.join(
......
...@@ -57,30 +57,32 @@ class BuildIOSCommand extends _BuildIOSSubCommand { ...@@ -57,30 +57,32 @@ class BuildIOSCommand extends _BuildIOSSubCommand {
Directory _outputAppDirectory(String xcodeResultOutput) => globals.fs.directory(xcodeResultOutput).parent; Directory _outputAppDirectory(String xcodeResultOutput) => globals.fs.directory(xcodeResultOutput).parent;
} }
/// The key that uniquely identifies an image file in an app icon asset. /// The key that uniquely identifies an image file in an image asset.
/// It consists of (idiom, size, scale). /// It consists of (idiom, scale, size?), where size is present for app icon
/// asset, and null for launch image asset.
@immutable @immutable
class _AppIconImageFileKey { class _ImageAssetFileKey {
const _AppIconImageFileKey(this.idiom, this.size, this.scale); const _ImageAssetFileKey(this.idiom, this.scale, this.size);
/// The idiom (iphone or ipad). /// The idiom (iphone or ipad).
final String idiom; final String idiom;
/// The logical size in point (e.g. 83.5).
final double size;
/// The scale factor (e.g. 2). /// The scale factor (e.g. 2).
final int scale; final int scale;
/// The logical size in point (e.g. 83.5).
/// Size is present for app icon, and null for launch image.
final double? size;
@override @override
int get hashCode => Object.hash(idiom, size, scale); int get hashCode => Object.hash(idiom, scale, size);
@override @override
bool operator ==(Object other) => other is _AppIconImageFileKey bool operator ==(Object other) => other is _ImageAssetFileKey
&& other.idiom == idiom && other.idiom == idiom
&& other.size == size && other.scale == scale
&& other.scale == scale; && other.size == size;
/// The pixel size. /// The pixel size based on logical size and scale.
int get pixelSize => (size * scale).toInt(); // pixel size must be an int. int? get pixelSize => size == null ? null : (size! * scale).toInt(); // pixel size must be an int.
} }
/// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for /// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for
...@@ -159,11 +161,16 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -159,11 +161,16 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
return super.validateCommand(); return super.validateCommand();
} }
// Parses Contents.json into a map, with the key to be _AppIconImageFileKey, and value to be the icon image file name. // A helper function to parse Contents.json of an image asset into a map,
Map<_AppIconImageFileKey, String> _parseIconContentsJson(String contentsJsonDirName) { // with the key to be _ImageAssetFileKey, and value to be the image file name.
// Some assets have size (e.g. app icon) and others do not (e.g. launch image).
Map<_ImageAssetFileKey, String> _parseImageAssetContentsJson(
String contentsJsonDirName,
{ required bool requiresSize })
{
final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName); final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName);
if (!contentsJsonDirectory.existsSync()) { if (!contentsJsonDirectory.existsSync()) {
return <_AppIconImageFileKey, String>{}; return <_ImageAssetFileKey, String>{};
} }
final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json'); final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json');
final Map<String, dynamic> contents = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>? ?? <String, dynamic>{}; final Map<String, dynamic> contents = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>? ?? <String, dynamic>{};
...@@ -171,10 +178,10 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -171,10 +178,10 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
final Map<String, dynamic> info = contents['info'] as Map<String, dynamic>? ?? <String, dynamic>{}; final Map<String, dynamic> info = contents['info'] as Map<String, dynamic>? ?? <String, dynamic>{};
if ((info['version'] as int?) != 1) { if ((info['version'] as int?) != 1) {
// Skips validation for unknown format. // Skips validation for unknown format.
return <_AppIconImageFileKey, String>{}; return <_ImageAssetFileKey, String>{};
} }
final Map<_AppIconImageFileKey, String> iconInfo = <_AppIconImageFileKey, String>{}; final Map<_ImageAssetFileKey, String> iconInfo = <_ImageAssetFileKey, String>{};
for (final dynamic image in images) { for (final dynamic image in images) {
final Map<String, dynamic> imageMap = image as Map<String, dynamic>; final Map<String, dynamic> imageMap = image as Map<String, dynamic>;
final String? idiom = imageMap['idiom'] as String?; final String? idiom = imageMap['idiom'] as String?;
...@@ -182,10 +189,15 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -182,10 +189,15 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
final String? scale = imageMap['scale'] as String?; final String? scale = imageMap['scale'] as String?;
final String? fileName = imageMap['filename'] as String?; final String? fileName = imageMap['filename'] as String?;
if (size == null || idiom == null || scale == null || fileName == null) { // requiresSize must match the actual presence of size in json.
if (requiresSize != (size != null)
|| idiom == null || scale == null || fileName == null)
{
continue; continue;
} }
final double? parsedSize;
if (size != null) {
// for example, "64x64". Parse the width since it is a square. // for example, "64x64". Parse the width since it is a square.
final Iterable<double> parsedSizes = size.split('x') final Iterable<double> parsedSizes = size.split('x')
.map((String element) => double.tryParse(element)) .map((String element) => double.tryParse(element))
...@@ -193,7 +205,10 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -193,7 +205,10 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
if (parsedSizes.isEmpty) { if (parsedSizes.isEmpty) {
continue; continue;
} }
final double parsedSize = parsedSizes.first; parsedSize = parsedSizes.first;
} else {
parsedSize = null;
}
// for example, "3x". // for example, "3x".
final Iterable<int> parsedScales = scale.split('x') final Iterable<int> parsedScales = scale.split('x')
...@@ -203,64 +218,113 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -203,64 +218,113 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
continue; continue;
} }
final int parsedScale = parsedScales.first; final int parsedScale = parsedScales.first;
iconInfo[_ImageAssetFileKey(idiom, parsedScale, parsedSize)] = fileName;
iconInfo[_AppIconImageFileKey(idiom, parsedSize, parsedScale)] = fileName;
} }
return iconInfo; return iconInfo;
} }
Future<void> _validateIconsAfterArchive(StringBuffer messageBuffer) async { // A helper function to check if an image asset is still using template files.
final BuildableIOSApp app = await buildableIOSApp; bool _isAssetStillUsingTemplateFiles({
final String templateIconImageDirName = await app.templateAppIconDirNameForImages; required Map<_ImageAssetFileKey, String> templateImageInfoMap,
required Map<_ImageAssetFileKey, String> projectImageInfoMap,
final Map<_AppIconImageFileKey, String> templateIconMap = _parseIconContentsJson(app.templateAppIconDirNameForContentsJson); required String templateImageDirName,
final Map<_AppIconImageFileKey, String> projectIconMap = _parseIconContentsJson(app.projectAppIconDirName); required String projectImageDirName,
}) {
// validate each of the project icon images. return projectImageInfoMap.entries.any((MapEntry<_ImageAssetFileKey, String> entry) {
final List<String> filesWithTemplateIcon = <String>[]; final String projectFileName = entry.value;
final List<String> filesWithWrongSize = <String>[]; final String? templateFileName = templateImageInfoMap[entry.key];
for (final MapEntry<_AppIconImageFileKey, String> entry in projectIconMap.entries) { if (templateFileName == null) {
final String projectIconFileName = entry.value; return false;
final String? templateIconFileName = templateIconMap[entry.key]; }
final File projectIconFile = globals.fs.file(globals.fs.path.join(app.projectAppIconDirName, projectIconFileName)); final File projectFile = globals.fs.file(
if (!projectIconFile.existsSync()) { globals.fs.path.join(projectImageDirName, projectFileName));
continue; final File templateFile = globals.fs.file(
globals.fs.path.join(templateImageDirName, templateFileName));
return projectFile.existsSync()
&& templateFile.existsSync()
&& md5.convert(projectFile.readAsBytesSync()) ==
md5.convert(templateFile.readAsBytesSync());
});
} }
final Uint8List projectIconBytes = projectIconFile.readAsBytesSync();
// validate conflict with template icon file. // A helper function to return a list of image files in an image asset with
if (templateIconFileName != null) { // wrong sizes (as specified in its Contents.json file).
final File templateIconFile = globals.fs.file(globals.fs.path.join( List<String> _imageFilesWithWrongSize({
templateIconImageDirName, templateIconFileName)); required Map<_ImageAssetFileKey, String> imageInfoMap,
if (templateIconFile.existsSync() && md5.convert(projectIconBytes) == required String imageDirName,
md5.convert(templateIconFile.readAsBytesSync())) { }) {
filesWithTemplateIcon.add(entry.value); return imageInfoMap.entries.where((MapEntry<_ImageAssetFileKey, String> entry) {
final String fileName = entry.value;
final File imageFile = globals.fs.file(globals.fs.path.join(imageDirName, fileName));
if (!imageFile.existsSync()) {
return false;
} }
}
// validate image size is correct. // validate image size is correct.
// PNG file's width is at byte [16, 20), and height is at byte [20, 24), in big endian format. // PNG file's width is at byte [16, 20), and height is at byte [20, 24), in big endian format.
// Based on https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format // Based on https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format
final ByteData projectIconData = projectIconBytes.buffer.asByteData(); final ByteData imageData = imageFile.readAsBytesSync().buffer.asByteData();
if (projectIconData.lengthInBytes < 24) { if (imageData.lengthInBytes < 24) {
continue; return false;
}
final int width = projectIconData.getInt32(16);
final int height = projectIconData.getInt32(20);
if (width != entry.key.pixelSize || height != entry.key.pixelSize) {
filesWithWrongSize.add(entry.value);
} }
final int width = imageData.getInt32(16);
final int height = imageData.getInt32(20);
// The size must not be null.
final int expectedSize = entry.key.pixelSize!;
return width != expectedSize || height != expectedSize;
})
.map((MapEntry<_ImageAssetFileKey, String> entry) => entry.value)
.toList();
} }
if (filesWithTemplateIcon.isNotEmpty) { Future<void> _validateIconAssetsAfterArchive(StringBuffer messageBuffer) async {
final BuildableIOSApp app = await buildableIOSApp;
final Map<_ImageAssetFileKey, String> templateInfoMap = _parseImageAssetContentsJson(
app.templateAppIconDirNameForContentsJson,
requiresSize: true);
final Map<_ImageAssetFileKey, String> projectInfoMap = _parseImageAssetContentsJson(
app.projectAppIconDirName,
requiresSize: true);
final bool usesTemplate = _isAssetStillUsingTemplateFiles(
templateImageInfoMap: templateInfoMap,
projectImageInfoMap: projectInfoMap,
templateImageDirName: await app.templateAppIconDirNameForImages,
projectImageDirName: app.projectAppIconDirName);
if (usesTemplate) {
messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.'); messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.');
} }
final List<String> filesWithWrongSize = _imageFilesWithWrongSize(
imageInfoMap: projectInfoMap,
imageDirName: app.projectAppIconDirName);
if (filesWithWrongSize.isNotEmpty) { if (filesWithWrongSize.isNotEmpty) {
messageBuffer.writeln('\nWarning: App icon is using the wrong size (e.g. ${filesWithWrongSize.first}).'); messageBuffer.writeln('\nWarning: App icon is using the wrong size (e.g. ${filesWithWrongSize.first}).');
} }
} }
Future<void> _validateLaunchImageAssetsAfterArchive(StringBuffer messageBuffer) async {
final BuildableIOSApp app = await buildableIOSApp;
final Map<_ImageAssetFileKey, String> templateInfoMap = _parseImageAssetContentsJson(
app.templateLaunchImageDirNameForContentsJson,
requiresSize: false);
final Map<_ImageAssetFileKey, String> projectInfoMap = _parseImageAssetContentsJson(
app.projectLaunchImageDirName,
requiresSize: false);
final bool usesTemplate = _isAssetStillUsingTemplateFiles(
templateImageInfoMap: templateInfoMap,
projectImageInfoMap: projectInfoMap,
templateImageDirName: await app.templateLaunchImageDirNameForImages,
projectImageDirName: app.projectLaunchImageDirName);
if (usesTemplate) {
messageBuffer.writeln('\nWarning: Launch image is set to the default placeholder. Replace with unique launch images.');
}
}
Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async { Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async {
final BuildableIOSApp app = await buildableIOSApp; final BuildableIOSApp app = await buildableIOSApp;
...@@ -296,7 +360,9 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -296,7 +360,9 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
final StringBuffer validationMessageBuffer = StringBuffer(); final StringBuffer validationMessageBuffer = StringBuffer();
await _validateXcodeBuildSettingsAfterArchive(validationMessageBuffer); await _validateXcodeBuildSettingsAfterArchive(validationMessageBuffer);
await _validateIconsAfterArchive(validationMessageBuffer); await _validateIconAssetsAfterArchive(validationMessageBuffer);
await _validateLaunchImageAssetsAfterArchive(validationMessageBuffer);
validationMessageBuffer.write('\nTo update the settings, please refer to https://docs.flutter.dev/deployment/ios'); validationMessageBuffer.write('\nTo update the settings, please refer to https://docs.flutter.dev/deployment/ios');
globals.printBox(validationMessageBuffer.toString(), title: 'App Settings'); globals.printBox(validationMessageBuffer.toString(), title: 'App Settings');
......
...@@ -153,30 +153,21 @@ class BuildableIOSApp extends IOSApp { ...@@ -153,30 +153,21 @@ class BuildableIOSApp extends IOSApp {
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!, _hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
'Info.plist'); 'Info.plist');
// Both project icon's image assets and Contents.json are in the same directory. String get projectAppIconDirName => _projectImageAssetDirName(_appIconAsset);
String get projectAppIconDirName => globals.fs.path.join('ios', _appIconDirNameSuffix);
// template icon's Contents.json is in flutter_tools. String get projectLaunchImageDirName => _projectImageAssetDirName(_launchImageAsset);
String get templateAppIconDirNameForContentsJson => globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'templates',
'app_shared',
'ios.tmpl',
_appIconDirNameSuffix,
);
// template icon's image assets are in flutter_template_images package. String get templateAppIconDirNameForContentsJson
Future<String> get templateAppIconDirNameForImages async { => _templateImageAssetDirNameForContentsJson(_appIconAsset);
final Directory imageTemplate = await templateImageDirectory(null, globals.fs, globals.logger);
return globals.fs.path.join( String get templateLaunchImageDirNameForContentsJson
imageTemplate.path, => _templateImageAssetDirNameForContentsJson(_launchImageAsset);
'app_shared',
'ios.tmpl', Future<String> get templateAppIconDirNameForImages async
_appIconDirNameSuffix, => _templateImageAssetDirNameForImages(_appIconAsset);
);
} Future<String> get templateLaunchImageDirNameForImages async
=> _templateImageAssetDirNameForImages(_launchImageAsset);
String get ipaOutputPath => String get ipaOutputPath =>
globals.fs.path.join(getIosBuildDirectory(), 'ipa'); globals.fs.path.join(getIosBuildDirectory(), 'ipa');
...@@ -185,10 +176,35 @@ class BuildableIOSApp extends IOSApp { ...@@ -185,10 +176,35 @@ class BuildableIOSApp extends IOSApp {
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName); return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
} }
String get _appIconDirNameSuffix => globals.fs.path.join( String _projectImageAssetDirName(String asset)
=> globals.fs.path.join('ios', 'Runner', 'Assets.xcassets', asset);
// Template asset's Contents.json file is in flutter_tools, but the actual
String _templateImageAssetDirNameForContentsJson(String asset)
=> globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'templates',
_templateImageAssetDirNameSuffix(asset),
);
// Template asset's images are in flutter_template_images package.
Future<String> _templateImageAssetDirNameForImages(String asset) async {
final Directory imageTemplate = await templateImageDirectory(null, globals.fs, globals.logger);
return globals.fs.path.join(imageTemplate.path, _templateImageAssetDirNameSuffix(asset));
}
String _templateImageAssetDirNameSuffix(String asset) => globals.fs.path.join(
'app_shared',
'ios.tmpl',
'Runner', 'Runner',
'Assets.xcassets', 'Assets.xcassets',
'AppIcon.appiconset'); asset,
);
String get _appIconAsset => 'AppIcon.appiconset';
String get _launchImageAsset => 'LaunchImage.imageset';
} }
class PrebuiltIOSApp extends IOSApp implements PrebuiltApplicationPackage { class PrebuiltIOSApp extends IOSApp implements PrebuiltApplicationPackage {
......
...@@ -1476,6 +1476,162 @@ void main() { ...@@ -1476,6 +1476,162 @@ void main() {
Platform: () => macosPlatform, Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
}); });
testUsingContext('Validate template launch images with conflicts', () async {
const String projectLaunchImageContentsJsonPath = 'ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json';
const String projectLaunchImagePath = 'ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png';
final String templateLaunchImageContentsJsonPath = '${Cache.flutterRoot!}/packages/flutter_tools/templates/app_shared/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json';
const String templateLaunchImagePath = '/flutter_template_images/templates/app_shared/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png';
fakeProcessManager.addCommands(<FakeCommand>[
xattrCommand,
setUpFakeXcodeBuildHandler(onRun: () {
fileSystem.file(templateLaunchImageContentsJsonPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"images": [
{
"idiom": "iphone",
"filename": "LaunchImage@2x.png",
"scale": "2x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
''');
fileSystem.file(templateLaunchImagePath)
..createSync(recursive: true)
..writeAsBytes(<int>[1, 2, 3]);
fileSystem.file(projectLaunchImageContentsJsonPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"images": [
{
"idiom": "iphone",
"filename": "LaunchImage@2x.png",
"scale": "2x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
''');
fileSystem.file(projectLaunchImagePath)
..createSync(recursive: true)
..writeAsBytes(<int>[1, 2, 3]);
}),
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
]);
createMinimalMockProjectFiles();
final BuildCommand command = BuildCommand(
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
);
await createTestCommandRunner(command).run(
<String>['build', 'ipa', '--no-pub']);
expect(
testLogger.statusText,
contains('Warning: Launch image is set to the default placeholder. Replace with unique launch images.'),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => fakeProcessManager,
Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('Validate template launch images without conflicts', () async {
const String projectLaunchImageContentsJsonPath = 'ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json';
const String projectLaunchImagePath = 'ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png';
final String templateLaunchImageContentsJsonPath = '${Cache.flutterRoot!}/packages/flutter_tools/templates/app_shared/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json';
const String templateLaunchImagePath = '/flutter_template_images/templates/app_shared/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png';
fakeProcessManager.addCommands(<FakeCommand>[
xattrCommand,
setUpFakeXcodeBuildHandler(onRun: () {
fileSystem.file(templateLaunchImageContentsJsonPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"images": [
{
"idiom": "iphone",
"filename": "LaunchImage@2x.png",
"scale": "2x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
''');
fileSystem.file(templateLaunchImagePath)
..createSync(recursive: true)
..writeAsBytes(<int>[1, 2, 3]);
fileSystem.file(projectLaunchImageContentsJsonPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"images": [
{
"idiom": "iphone",
"filename": "LaunchImage@2x.png",
"scale": "2x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
''');
fileSystem.file(projectLaunchImagePath)
..createSync(recursive: true)
..writeAsBytes(<int>[4, 5, 6]);
}),
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
]);
createMinimalMockProjectFiles();
final BuildCommand command = BuildCommand(
androidSdk: FakeAndroidSdk(),
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
);
await createTestCommandRunner(command).run(
<String>['build', 'ipa', '--no-pub']);
expect(
testLogger.statusText,
isNot(contains('Warning: Launch image is set to the default placeholder. Replace with unique launch images.')),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => fakeProcessManager,
Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
} }
......
...@@ -476,6 +476,92 @@ void main() { ...@@ -476,6 +476,92 @@ void main() {
), ),
); );
}, overrides: overrides); }, overrides: overrides);
testUsingContext('returns project launch image dirname', () async {
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner',
);
final String launchImageDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'LaunchImage.imageset',
);
expect(iosApp.projectLaunchImageDirName, globals.fs.path.join('ios', launchImageDirSuffix));
}, overrides: overrides);
testUsingContext('returns template launch image dirname for Contents.json', () async {
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner',
);
final String launchImageDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'LaunchImage.imageset',
);
expect(
iosApp.templateLaunchImageDirNameForContentsJson,
globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'templates',
'app_shared',
'ios.tmpl',
launchImageDirSuffix,
),
);
}, overrides: overrides);
testUsingContext('returns template launch image dirname for images', () async {
final String toolsDir = globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
);
final String packageConfigPath = globals.fs.path.join(
toolsDir,
'.dart_tool',
'package_config.json'
);
globals.fs.file(packageConfigPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "flutter_template_images",
"rootUri": "/flutter_template_images",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner');
final String launchImageDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'LaunchImage.imageset',
);
expect(
await iosApp.templateLaunchImageDirNameForImages,
globals.fs.path.absolute(
'flutter_template_images',
'templates',
'app_shared',
'ios.tmpl',
launchImageDirSuffix,
),
);
}, overrides: overrides);
}); });
group('FuchsiaApp', () { group('FuchsiaApp', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment