-
-
Save aednlaxer/3a563979e653c9b9640b6a65c1205dea to your computer and use it in GitHub Desktop.
Flutter widget tests to create app store graphics
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:device_frame/device_frame.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
/// A widget that renders a [child] with [text] on top | |
class AppStoreFrame extends StatelessWidget { | |
const AppStoreFrame({ | |
required this.text, | |
required this.child, | |
required this.deviceInfo, | |
super.key, | |
}); | |
final String text; | |
final Widget child; | |
final DeviceInfo deviceInfo; | |
@override | |
Widget build(BuildContext context) { | |
final backgroundColor = debugBrightnessOverride == Brightness.dark | |
? Colors.black | |
: Colors.white; | |
final textColor = debugBrightnessOverride == Brightness.dark | |
? Colors.white | |
: Colors.black; | |
return MediaQuery( | |
data: const MediaQueryData(), | |
child: Directionality( | |
textDirection: TextDirection.ltr, | |
child: ColoredBox( | |
color: backgroundColor, | |
child: Stack( | |
children: [ | |
Container( | |
height: 140, | |
alignment: Alignment.center, | |
padding: const EdgeInsets.only( | |
top: 24, | |
left: 16, | |
right: 16, | |
), | |
child: Text( | |
text, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
fontSize: 24, | |
fontFamily: 'Fira', | |
color: textColor, | |
), | |
), | |
), | |
Positioned( | |
top: 170, | |
left: 16, | |
right: 16, | |
child: DeviceFrame( | |
device: deviceInfo, | |
screen: Container( | |
color: backgroundColor, | |
padding: deviceInfo.safeAreas, | |
child: child, | |
), | |
), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:ratiom8/model/preferences_model.dart'; | |
/// A mock version of this app's PreferenceModel that returns the given | |
/// values for [getRatio] and [getBeans]. | |
class MockPreferencesModel extends PreferencesModel { | |
MockPreferencesModel(this._ratio, this._beans); | |
final double _ratio; | |
final double _beans; | |
@override | |
Future<double?> getRatio() async => _ratio; | |
@override | |
Future<double?> getBeans() async => _beans; | |
@override | |
Future<void> saveRatioBeansWater(double ratio, double beans) async { | |
return; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'dart:ui' as ui; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter_test/flutter_test.dart'; | |
const _directory = 'app-store-screenshots'; | |
Future<void> takeWidgetScreenshot( | |
WidgetTester tester, | |
Finder finder, | |
String filename, | |
double pixelRatio, | |
) async { | |
final boundary = tester.renderObject(finder); | |
if (boundary is! RenderRepaintBoundary) { | |
throw Exception('Widget is not a RenderRepaintBoundary'); | |
} | |
final image = await boundary.toImage(pixelRatio: pixelRatio); | |
await Directory(_directory).create(); | |
final file = File('$_directory/$filename'); | |
final byteData = await image.toByteData(format: ui.ImageByteFormat.png); | |
await file.writeAsBytes(byteData!.buffer.asUint8List()); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:device_frame/device_frame.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
import 'package:flutter_test/flutter_test.dart'; | |
import 'package:ratiom8/app/app.dart'; | |
import 'package:ratiom8/model/preferences_model.dart'; | |
import 'infrastructure/app_store_frame.dart'; | |
import 'infrastructure/mock_preferences_model.dart'; | |
import 'infrastructure/screenshot.dart'; | |
const iphoneXsMax = Size(414, 896); | |
const iphone8Plus = Size(414, 736); | |
const iPad3G = Size(1366, 1024); | |
const iPad2G = Size(1366, 1024); | |
Future<void> main() async { | |
setUp(() async { | |
// Load app fonts so text doesn't look redacted | |
final font = rootBundle.load('assets/fonts/fira_code_regular.ttf'); | |
final fontLoader = FontLoader('Fira')..addFont(font); | |
await fontLoader.load(); | |
}); | |
testWidgets( | |
'Create App Store screenshots', | |
(tester) async { | |
await takeScreenshots(tester, iphoneXsMax, Devices.ios.iPhone13, 3); | |
await takeScreenshots(tester, iphone8Plus, Devices.ios.iPhoneSE, 3); | |
await takeScreenshots(tester, iPad2G, Devices.ios.iPad12InchesGen2, 2); | |
await takeScreenshots(tester, iPad3G, Devices.ios.iPad12InchesGen4, 2); | |
}, | |
); | |
} | |
/// Takes a series of 3 screenshots for the given device | |
Future<void> takeScreenshots( | |
WidgetTester tester, | |
Size imageSize, | |
DeviceInfo deviceInfo, | |
double pixelRatio, | |
) async { | |
await tester.runAsync(() async { | |
await takeAppScreenshot( | |
tester: tester, | |
imageSize: imageSize, | |
deviceInfo: deviceInfo, | |
filename: '${deviceInfo.name}-first.png', | |
model: MockPreferencesModel(17, 30), | |
brightness: Brightness.light, | |
text: 'Brew a perfect cup of coffee!', | |
pixelRatio: pixelRatio, | |
); | |
await takeAppScreenshot( | |
tester: tester, | |
imageSize: imageSize, | |
deviceInfo: deviceInfo, | |
filename: '${deviceInfo.name}-second.png', | |
model: MockPreferencesModel(16.5, 24), | |
brightness: Brightness.light, | |
text: 'Set ratio, beans and water', | |
pixelRatio: pixelRatio, | |
); | |
await takeAppScreenshot( | |
tester: tester, | |
imageSize: imageSize, | |
deviceInfo: deviceInfo, | |
filename: '${deviceInfo.name}-third.png', | |
model: MockPreferencesModel(17, 30), | |
brightness: Brightness.dark, | |
text: 'Dark theme for a midnight cup of Joe', | |
pixelRatio: pixelRatio, | |
); | |
}); | |
} | |
/// Captures a screenshot of the app widget with the given parameters | |
Future<void> takeAppScreenshot({ | |
required WidgetTester tester, | |
required Size imageSize, | |
required DeviceInfo deviceInfo, | |
required String filename, | |
required MockPreferencesModel model, | |
required Brightness brightness, | |
required String text, | |
required double pixelRatio, | |
}) async { | |
await tester.binding.setSurfaceSize(imageSize); | |
// A trick to create a new unique key for each function call | |
final rootKey = ValueKey('key-${DateTime.now()}'); | |
debugBrightnessOverride = brightness; | |
await tester.pumpWidget( | |
RepaintBoundary( | |
key: rootKey, | |
// Add custom app store frame (text and device frame) | |
child: AppStoreFrame( | |
deviceInfo: deviceInfo, | |
text: text, | |
child: ProviderScope( | |
// Override state with custom value | |
overrides: [preferenceModelProvider.overrideWithValue(model)], | |
// This is the main app widget, not shared in this gist | |
child: const App(), | |
), | |
), | |
), | |
); | |
await tester.pumpAndSettle(); | |
await takeWidgetScreenshot(tester, find.byKey(rootKey), filename, pixelRatio); | |
debugBrightnessOverride = null; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment