diff --git a/screenshots/assets/frames/ios/iphone_13_pro_frame.png b/screenshots/assets/frames/ios/iphone_13_pro_frame.png new file mode 100644 index 0000000..69d871f Binary files /dev/null and b/screenshots/assets/frames/ios/iphone_13_pro_frame.png differ diff --git a/screenshots/devices_config.dart b/screenshots/devices_config.dart index 2cc7a3e..f1805e2 100644 --- a/screenshots/devices_config.dart +++ b/screenshots/devices_config.dart @@ -1,26 +1,37 @@ enum ScreenshotDevicePlatform { android, ios } +final screenshotDevicesIos = [ + ScreenshotDevice.fromDisplayName( + displayName: 'iPhone 8 Plus', + platform: ScreenshotDevicePlatform.ios, + ), + ScreenshotDevice.fromDisplayName( + displayName: 'iPhone 13 Pro', + platform: ScreenshotDevicePlatform.ios, + screenshotFrameOffset: (dx: 72, dy: 60), + ), +]; + class ScreenshotDevice { final String name; final ScreenshotDevicePlatform platform; + final ({int dx, int dy}) screenshotFrameOffset; const ScreenshotDevice({ required this.name, required this.platform, + this.screenshotFrameOffset = (dx: 0, dy: 0), }); ScreenshotDevice.fromDisplayName({ required String displayName, required this.platform, + this.screenshotFrameOffset = (dx: 0, dy: 0), }) : name = displayName.replaceAll(' ', '_').toLowerCase(); String get systemOverlayPathLight => 'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_light.png'; String get systemOverlayPathDark => 'screenshots/assets/system_overlays/${platform.name}/${name}_system_overlay_dark.png'; + String get deviceFramePath => 'screenshots/assets/frames/${platform.name}/${name}_frame.png'; } - -final screenshotDevicesIos = [ - ScreenshotDevice.fromDisplayName(displayName: 'iPhone 8 Plus', platform: ScreenshotDevicePlatform.ios), - ScreenshotDevice.fromDisplayName(displayName: 'iPhone 13 Pro', platform: ScreenshotDevicePlatform.ios), -]; diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index 48d45bd..f9d574f 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -19,17 +19,20 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../integration_test/mocks/paid_features_mock.dart'; import '../integration_test/utils/widget_tester_actions.dart'; +import 'screenshot_args.dart'; //https://stackoverflow.com/a/67186625/13167574 const _mockFilm = Film('Ilford HP5+', 400); +final Color _lightThemeColor = primaryColorsList[5]; +final Color _darkThemeColor = primaryColorsList[3]; +final ThemeData _themeLight = themeFrom(_lightThemeColor, Brightness.light); +final ThemeData _themeDark = themeFrom(_darkThemeColor, Brightness.dark); /// Just a screenshot generator. No expectations here. void main() { final binding = IntegrationTestWidgetsFlutterBinding(); IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final Color lightThemeColor = primaryColorsList[5]; - final Color darkThemeColor = primaryColorsList[3]; void mockSharedPrefs(ThemeType theme, Color color) { // ignore: invalid_use_of_visible_for_testing_member @@ -70,7 +73,7 @@ void main() { /// Generates several screenshots with the light theme testWidgets('Generate light theme screenshots', (tester) async { - mockSharedPrefs(ThemeType.light, lightThemeColor); + mockSharedPrefs(ThemeType.light, _lightThemeColor); await tester.pumpApplication( availableFilms: [_mockFilm], filmsInUse: [_mockFilm], @@ -114,7 +117,7 @@ void main() { testWidgets( 'Generate dark theme screenshots', (tester) async { - mockSharedPrefs(ThemeType.dark, darkThemeColor); + mockSharedPrefs(ThemeType.dark, _darkThemeColor); await tester.pumpApplication( availableFilms: [_mockFilm], filmsInUse: [_mockFilm], @@ -138,8 +141,21 @@ final String _platformFolder = Platform.isAndroid ? 'android' : 'ios'; extension on WidgetTester { Future takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async { + final bool isDark = name.contains('dark-'); + final Color backgroundColor = (isDark ? _themeDark : _themeLight).colorScheme.surface; await binding.takeScreenshot( - "$_platformFolder/${const String.fromEnvironment('deviceName').replaceAll(' ', '_').toLowerCase()}/$name", + ScreenshotArgs( + name: name, + deviceName: const String.fromEnvironment('deviceName').replaceAll(' ', '_').toLowerCase(), + platformFolder: _platformFolder, + backgroundColor: ( + r: backgroundColor.red, + g: backgroundColor.green, + b: backgroundColor.blue, + a: backgroundColor.alpha, + ), + isDark: isDark, + ).toString(), ); await pumpAndSettle(); } diff --git a/screenshots/screenshot_args.dart b/screenshots/screenshot_args.dart new file mode 100644 index 0000000..ef11379 --- /dev/null +++ b/screenshots/screenshot_args.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +class ScreenshotArgs { + final String name; + final String deviceName; + final String platformFolder; + final ({int r, int g, int b, int a}) backgroundColor; + final bool isDark; + + const ScreenshotArgs({ + required this.name, + required this.deviceName, + required this.platformFolder, + required this.backgroundColor, + required this.isDark, + }); + + String toPath() => 'screenshots/generated/$platformFolder/$deviceName/$name.png'; + + @override + String toString() => jsonEncode(_toJson()); + + factory ScreenshotArgs.fromString(String data) => ScreenshotArgs._fromJson(jsonDecode(data) as Map); + + factory ScreenshotArgs._fromJson(Map data) { + final colorChannels = data['backgroundColor'] as List; + return ScreenshotArgs( + name: data['name'] as String, + deviceName: data['deviceName'] as String, + platformFolder: data['platformFolder'] as String, + backgroundColor: ( + r: colorChannels[0] as int, + g: colorChannels[1] as int, + b: colorChannels[2] as int, + a: colorChannels[3] as int, + ), + isDark: data['isDark'] as bool, + ); + } + + Map _toJson() { + return { + "name": name, + "deviceName": deviceName, + "platformFolder": platformFolder, + "backgroundColor": [ + backgroundColor.r, + backgroundColor.g, + backgroundColor.b, + backgroundColor.a, + ], + "isDark": isDark, + }; + } +} diff --git a/test_driver/screenshot_driver.dart b/test_driver/screenshot_driver.dart index fe49e36..99e9685 100644 --- a/test_driver/screenshot_driver.dart +++ b/test_driver/screenshot_driver.dart @@ -1,15 +1,83 @@ import 'dart:io'; +import 'dart:typed_data'; + +import 'package:image/image.dart'; import 'package:integration_test/integration_test_driver_extended.dart'; +import '../screenshots/devices_config.dart'; +import '../screenshots/screenshot_args.dart'; import 'utils/grant_camera_permission.dart'; Future main() async { await grantCameraPermission(); await integrationDriver( - onScreenshot: (name, bytes, [args]) async { - final File image = await File('screenshots/generated/$name.png').create(recursive: true); - image.writeAsBytesSync(bytes); + onScreenshot: (name, bytes, [_]) async { + final screenshotArgs = ScreenshotArgs.fromString(name); + final backgroundColor = ColorRgba8( + screenshotArgs.backgroundColor.r, + screenshotArgs.backgroundColor.g, + screenshotArgs.backgroundColor.b, + screenshotArgs.backgroundColor.a, + ); + final platform = + screenshotArgs.platformFolder == 'ios' ? ScreenshotDevicePlatform.ios : ScreenshotDevicePlatform.ios; + final deviceName = screenshotArgs.deviceName; + final file = await File(screenshotArgs.toPath()).create(recursive: true); + + // switch (platform) { + // case ScreenshotDevicePlatform.ios: + // final device = screenshotDevicesIos.firstWhere( + // (device) => device.name == deviceName, + // orElse: () => ScreenshotDevice(name: '', platform: platform), + // ); + // Image screenshot = decodePng(Uint8List.fromList(bytes))!; + // screenshot = screenshot.addSystemOverlay(device, isDark: screenshotArgs.isDark); + // screenshot = screenshot.addDeviceFrame(device, backgroundColor); + // file.writeAsBytesSync(encodePng(screenshot)); + // case ScreenshotDevicePlatform.android: + // file.writeAsBytesSync(bytes); + // } + + file.writeAsBytesSync(bytes); return true; }, ); } + +extension ScreenshotImage on Image { + Image addSystemOverlay(ScreenshotDevice device, {required bool isDark}) { + final path = isDark ? device.systemOverlayPathDark : device.systemOverlayPathLight; + final statusBar = copyResize( + decodePng(File(path).readAsBytesSync())!, + width: width, + ); + return compositeImage(this, statusBar); + } + + Image addDeviceFrame(ScreenshotDevice device, Color backgroundColor) { + final screenshotRounded = copyCrop( + this, + x: 0, + y: 0, + width: width, + height: height, + ); + + final frame = decodePng(File(device.deviceFramePath).readAsBytesSync())!; + final expandedScreenshot = copyExpandCanvas( + copyExpandCanvas( + screenshotRounded, + newWidth: screenshotRounded.width + device.screenshotFrameOffset.dx, + newHeight: screenshotRounded.height + device.screenshotFrameOffset.dy, + position: ExpandCanvasPosition.bottomRight, + backgroundColor: backgroundColor, + ), + newWidth: frame.width, + newHeight: frame.height, + position: ExpandCanvasPosition.topLeft, + backgroundColor: backgroundColor, + ); + + return compositeImage(expandedScreenshot, frame); + } +}