Commit 31ec21a

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-08-11 11:56:53
Automate store screenshots (#385)
* Automate store screenshots Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com> * Update store screenshots Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com> --------- Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent bc734cd
app/integration_test/add_measurement_test.dart
@@ -16,7 +16,7 @@ void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
   testWidgets('Can enter value only measurements', (WidgetTester tester) async {
     final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-    await tester.pumpWidget(App(forceClearAppDataOnLaunch: true));
+    await tester.pumpWidget(App());
     await tester.pumpAndSettle();
     await tester.pumpUntil(() => find.byType(AppHome).hasFound);
     expect(find.byType(AppHome), findsOneWidget);
@@ -57,7 +57,7 @@ void main() {
 
   testWidgets('Can enter complex measurements', (WidgetTester tester) async {
     final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-    await tester.pumpWidget(App(forceClearAppDataOnLaunch: true,));
+    await tester.pumpWidget(App());
     await tester.pumpAndSettle();
     await tester.pumpUntil(() => find.byType(AppHome).hasFound);
     expect(find.byType(AppHome), findsOneWidget);
app/integration_test/navigation_test.dart
@@ -25,7 +25,7 @@ void main() {
     final localizations = await AppLocalizations.delegate.load(const Locale('en'));
     const double settingsScrollAmount = 200.0;
 
-    await tester.pumpWidget(App(forceClearAppDataOnLaunch: true));
+    await tester.pumpWidget(App());
     await tester.pumpAndSettle();
     await tester.pumpUntil(() => find.byType(AppHome).hasFound);
     // home
app/integration_test/screenshot_home.dart
@@ -0,0 +1,116 @@
+
+import 'package:blood_pressure_app/app.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util.dart';
+
+void main() {
+  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  testWidgets('Screenshot home page', (WidgetTester tester) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    await tester.pumpWidget(App());
+    await tester.pumpAndSettle();
+    await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+    
+    await tester.tap(find.text(localizations.last7Days));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(localizations.day));
+    await tester.pumpAndSettle();
+
+    await tester.enterMeasurement(sys: 119, dia: 75, pul: 65);
+    await tester.enterMeasurement(sys: 114, dia: 83, pul: 70, color: Colors.red);
+    await tester.enterMeasurement(sys: 107, dia: 73, pul: 64);
+    await tester.enterMeasurement(sys: 116, dia: 71, pul: 61, note: 'Add notes and colors!', color: Colors.lightBlue);
+    await tester.enterMeasurement(sys: 105, dia: 74, pul: 60);
+    await tester.pumpUntil(() => find.text('116').hasFound);
+    
+    await tester.tap(find.text('116'));
+    await tester.pumpAndSettle();
+
+    await binding.convertFlutterSurfaceToImage();
+    await tester.pump();
+    await binding.takeScreenshot('02-example_home');
+  });
+}
+
+extension on WidgetTester {
+  Future<void> enterMeasurement({
+    int? sys,
+    int? dia,
+    int? pul,
+    String? note,
+    Color? color,
+    bool save = true,
+  }) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    await tap(find.byIcon(Icons.add));
+    await pumpAndSettle();
+    if (sys != null) await enterText(find.byType(TextFormField).at(0), '$sys');
+    if (dia != null) await enterText(find.byType(TextFormField).at(1), '$dia');
+    if (pul != null) await enterText(find.byType(TextFormField).at(2), '$pul');
+    if (note != null) await enterText(find.byType(TextFormField).at(3), '$note');
+    if (color != null) {
+      await tap(find.text(localizations.color));
+      await pumpAndSettle();
+      await tap(find.byElementPredicate(_colored(color)));
+      await pumpAndSettle();
+    }
+
+    if (save) {
+      await tap(find.text(localizations.btnSave));
+      await pumpAndSettle();
+    }
+  }
+}
+
+bool Function(Element e) _colored(Color color) => (e) =>
+  e.widget is Container &&
+    (e.widget as Container).decoration is BoxDecoration &&
+      ((e.widget as Container).decoration as BoxDecoration).color == color;
+
+// Copy of app method
+ThemeData _buildTheme(ColorScheme colorScheme) {
+  final inputBorder = OutlineInputBorder(
+    borderSide: BorderSide(
+      width: 3,
+      // Through black background outlineVariant has enough contrast.
+      color: (colorScheme.brightness == Brightness.dark)
+          ? colorScheme.outlineVariant
+          : colorScheme.outline,
+    ),
+    borderRadius: BorderRadius.circular(20),
+  );
+
+  return ThemeData(
+    colorScheme: colorScheme,
+    useMaterial3: true,
+    inputDecorationTheme: InputDecorationTheme(
+      errorMaxLines: 5,
+      border: inputBorder,
+      enabledBorder: inputBorder,
+    ),
+    scaffoldBackgroundColor: colorScheme.brightness == Brightness.dark
+        ? Colors.black
+        : Colors.white,
+    appBarTheme: const AppBarTheme(
+      centerTitle: true,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.only(
+          bottomRight: Radius.circular(15),
+          bottomLeft: Radius.circular(15),
+        ),
+      ),
+    ),
+    snackBarTheme: SnackBarThemeData(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+    ),
+  );
+}
app/integration_test/screenshot_input.dart
@@ -0,0 +1,143 @@
+
+import 'package:blood_pressure_app/app.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util.dart';
+
+void main() {
+  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  testWidgets('Screenshot input dialoge', (WidgetTester tester) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+    const double settingsScrollAmount = 200.0;
+
+    await tester.pumpWidget(App());
+    await tester.pumpAndSettle();
+    await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+    // home
+
+    await tester.tap(find.byIcon(Icons.settings));
+    await tester.pumpAndSettle();
+    // settings
+
+    await tester.scrollUntilVisible(find.text(localizations.medications), settingsScrollAmount);
+    await tester.tap(find.text(localizations.medications));
+    await tester.pumpAndSettle();
+    // medication manager
+    await tester.tap(find.text(localizations.addMedication));
+    await tester.pumpAndSettle();
+    // add medication
+    await tester.enterText(find.byType(TextFormField).at(0), 'Metolazone');
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(localizations.color));
+    await tester.pumpAndSettle();
+    await tester.tap(find.byElementPredicate(_colored(Colors.teal)));
+    await tester.pumpAndSettle();
+
+    await tester.tap(find.text(localizations.btnSave));
+    await tester.pumpAndSettle();
+    // medication manager
+    await tester.tap(find.byIcon(Icons.arrow_back));
+    await tester.pumpAndSettle();
+    // settings
+    await tester.tap(find.byIcon(Icons.arrow_back));
+    await tester.pumpAndSettle();
+    // home
+
+    await tester.enterMeasurement(sys: 119, dia: 75, pul: 65,
+      note: 'Enter measurements faster than anywhere else!',
+      color: Colors.yellow,
+      save: false,
+    );
+    await tester.tap(find.text(localizations.noMedication));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text('Metolazone'));
+    await tester.pumpAndSettle();
+    await tester.enterText(find.byType(TextFormField).at(4), '1.5');
+
+    await tester.pumpAndSettle();
+    await binding.convertFlutterSurfaceToImage();
+    await tester.pump();
+    await binding.takeScreenshot('01-example_add');
+  });
+}
+
+extension on WidgetTester {
+  Future<void> enterMeasurement({
+    int? sys,
+    int? dia,
+    int? pul,
+    String? note,
+    Color? color,
+    bool save = true,
+  }) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    await tap(find.byIcon(Icons.add));
+    await pumpAndSettle();
+    if (sys != null) await enterText(find.byType(TextFormField).at(0), '$sys');
+    if (dia != null) await enterText(find.byType(TextFormField).at(1), '$dia');
+    if (pul != null) await enterText(find.byType(TextFormField).at(2), '$pul');
+    if (note != null) await enterText(find.byType(TextFormField).at(3), '$note');
+    if (color != null) {
+      await tap(find.text(localizations.color));
+      await pumpAndSettle();
+      await tap(find.byElementPredicate(_colored(color)));
+      await pumpAndSettle();
+    }
+
+    if (save) {
+      await tap(find.text(localizations.btnSave));
+      await pumpAndSettle();
+    }
+  }
+}
+
+bool Function(Element e) _colored(Color color) => (e) =>
+  e.widget is Container &&
+    (e.widget as Container).decoration is BoxDecoration &&
+      ((e.widget as Container).decoration as BoxDecoration).color == color;
+
+// Copy of app method
+ThemeData _buildTheme(ColorScheme colorScheme) {
+  final inputBorder = OutlineInputBorder(
+    borderSide: BorderSide(
+      width: 3,
+      // Through black background outlineVariant has enough contrast.
+      color: (colorScheme.brightness == Brightness.dark)
+          ? colorScheme.outlineVariant
+          : colorScheme.outline,
+    ),
+    borderRadius: BorderRadius.circular(20),
+  );
+
+  return ThemeData(
+    colorScheme: colorScheme,
+    useMaterial3: true,
+    inputDecorationTheme: InputDecorationTheme(
+      errorMaxLines: 5,
+      border: inputBorder,
+      enabledBorder: inputBorder,
+    ),
+    scaffoldBackgroundColor: colorScheme.brightness == Brightness.dark
+        ? Colors.black
+        : Colors.white,
+    appBarTheme: const AppBarTheme(
+      centerTitle: true,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.only(
+          bottomRight: Radius.circular(15),
+          bottomLeft: Radius.circular(15),
+        ),
+      ),
+    ),
+    snackBarTheme: SnackBarThemeData(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+    ),
+  );
+}
app/integration_test/screenshot_settings.dart
@@ -0,0 +1,42 @@
+
+import 'package:blood_pressure_app/app.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util.dart';
+
+void main() {
+  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  testWidgets('Screenshot settings', (WidgetTester tester) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    await tester.pumpWidget(App());
+    await tester.pumpAndSettle();
+    await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+    // home
+
+    await tester.tap(find.byIcon(Icons.settings));
+    await tester.pumpAndSettle();
+    // settings
+
+    await tester.dragFrom(
+      tester.getCenter(find.text(localizations.animationSpeed)),
+      tester.getCenter(find.text(localizations.animationSpeed)) - tester.getCenter(find.text(localizations.timeFormat))
+    );
+    await tester.pumpAndSettle();
+
+    await binding.convertFlutterSurfaceToImage();
+    await tester.pump();
+    await binding.takeScreenshot('03-example_settings');
+
+    await tester.scrollUntilVisible(find.text(localizations.exportImport, skipOffstage: false), 50.0);
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(localizations.exportImport));
+    await tester.pumpAndSettle();
+
+    await binding.takeScreenshot('05-export_example');
+  });
+}
app/integration_test/screenshot_stats.dart
@@ -0,0 +1,117 @@
+
+import 'dart:math';
+
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:blood_pressure_app/screens/statistics_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:intl/date_symbol_data_local.dart';
+import 'package:provider/provider.dart';
+
+import '../test/model/analyzer_test.dart';
+
+void main() {
+  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  testWidgets('Statistics screen', (WidgetTester tester) async {
+    await TestWidgetsFlutterBinding.ensureInitialized();
+    await initializeDateFormatting('en');
+    await tester.pumpWidget(MaterialApp(
+      darkTheme: _buildTheme(ColorScheme.fromSeed(
+        seedColor: Colors.teal,
+        brightness: Brightness.dark,
+      ),),
+      themeMode: ThemeMode.dark,
+      debugShowCheckedModeBanner: false,
+      localizationsDelegates: [AppLocalizations.delegate,], locale: Locale('en'),
+      home: MultiProvider(
+        providers: [
+          ChangeNotifierProvider(create: (c) => IntervallStoreManager(IntervallStorage(), IntervallStorage(), IntervallStorage())),
+          ChangeNotifierProvider(create: (c) => Settings()),
+        ],
+        child: RepositoryProvider<BloodPressureRepository>(
+          create: (c) {
+            final rng = Random();
+            final repo = _MockRepo();
+            repo.records = [
+              for (int i = 0; i < 144; i++)
+                mockRecord(
+                  time: DateTime.fromMillisecondsSinceEpoch(1000*60*60*24*265*40 + i * 1000*60*60*8),
+                  sys: 130 + (rng.nextInt(40) - 20) - (i ~/ 8),
+                  dia: 85 + (rng.nextInt(30) - 15) - (i ~/ 9),
+                  pul: 70 + (rng.nextInt(40) - 20) - (i ~/ 8),
+                ),
+            ];
+            return repo;
+          },
+          child: StatisticsScreen(),
+        ),
+      ),
+    ));
+
+    await tester.pumpAndSettle();
+    await binding.convertFlutterSurfaceToImage();
+    await tester.pump();
+    await binding.takeScreenshot('04-example_stats');
+  });
+}
+
+class _MockRepo extends BloodPressureRepository {
+  List<BloodPressureRecord> records = [];
+
+  @override
+  Future<void> add(BloodPressureRecord value) => throw UnimplementedError();
+
+  @override
+  Future<List<BloodPressureRecord>> get(DateRange range) async => records;
+
+  @override
+  Future<void> remove(BloodPressureRecord value) => throw UnimplementedError();
+
+  @override
+  Stream subscribe() => Stream.empty();
+}
+
+// Copy of app method
+ThemeData _buildTheme(ColorScheme colorScheme) {
+  final inputBorder = OutlineInputBorder(
+    borderSide: BorderSide(
+      width: 3,
+      // Through black background outlineVariant has enough contrast.
+      color: (colorScheme.brightness == Brightness.dark)
+          ? colorScheme.outlineVariant
+          : colorScheme.outline,
+    ),
+    borderRadius: BorderRadius.circular(20),
+  );
+
+  return ThemeData(
+    colorScheme: colorScheme,
+    useMaterial3: true,
+    inputDecorationTheme: InputDecorationTheme(
+      errorMaxLines: 5,
+      border: inputBorder,
+      enabledBorder: inputBorder,
+    ),
+    scaffoldBackgroundColor: colorScheme.brightness == Brightness.dark
+        ? Colors.black
+        : Colors.white,
+    appBarTheme: const AppBarTheme(
+      centerTitle: true,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.only(
+          bottomRight: Radius.circular(15),
+          bottomLeft: Radius.circular(15),
+        ),
+      ),
+    ),
+    snackBarTheme: SnackBarThemeData(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+    ),
+  );
+}
app/lib/app.dart
@@ -25,10 +25,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 /// that should be available everywhere in the app.
 class App extends StatefulWidget {
   /// Create the base for the entire app.
-  const App({this.forceClearAppDataOnLaunch = false});
-
-  /// Permanently deletes all files the app uses during state initialization.
-  final bool forceClearAppDataOnLaunch;
+  const App();
 
   @override
   State<App> createState() => _AppState();
@@ -67,14 +64,11 @@ class _AppState extends State<App> {
 
   /// Load the primary app data asynchronously to allow load animations.
   Future<Widget> _loadApp() async {
-    WidgetsFlutterBinding.ensureInitialized();
     if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
       databaseFactory = databaseFactoryFfi;
     }
 
-    if (_loadedChild != null && _configDB != null && _entryDB != null) return _loadedChild!;
-
-    if (widget.forceClearAppDataOnLaunch) {
+    if (!(const bool.fromEnvironment('testing_mode'))) {
       final dbPath = await getDatabasesPath();
       try {
         File(join(dbPath, 'bp.db')).deleteSync();
@@ -187,7 +181,10 @@ class _AppState extends State<App> {
 
   @override
   Widget build(BuildContext context) {
-    if (_loadedChild != null && _configDB != null && _entryDB != null) return _loadedChild!;
+    if (!(const bool.fromEnvironment('testing_mode'))
+        && _loadedChild != null && _configDB != null && _entryDB != null) {
+      return _loadedChild!;
+    }
     return ConsistentFutureBuilder(
       future: _loadApp(),
       onWaiting: const LoadingScreen(),
@@ -211,6 +208,7 @@ class _AppState extends State<App> {
       localizationsDelegates: AppLocalizations.localizationsDelegates,
       supportedLocales: AppLocalizations.supportedLocales,
       locale: settings.language,
+      debugShowCheckedModeBanner: false,
       home: const AppHome(),
     ),
   );
app/test_driver/integration_test.dart
@@ -1,3 +1,11 @@
-import 'package:integration_test/integration_test_driver.dart';
+import 'dart:io';
 
-Future<void> main() => integrationDriver();
\ No newline at end of file
+import 'package:integration_test/integration_test_driver_extended.dart';
+
+Future<void> main() async => integrationDriver(
+  onScreenshot: (String name, List<int> bytes, [Map<String, Object?>? args]) async {
+    Directory('build/screenshots').createSync(recursive: true);
+    File('build/screenshots/$name.png').writeAsBytesSync(bytes);
+    return true;
+  }
+);
docs/testing.md
@@ -31,12 +31,13 @@ To run integration tests an android emulator needs to be running. During develop
 flutter drive \
   --driver=test_driver/integration_test.dart \
   --target=integration_test/<testName>.dart \
+  --dart-define=testing_mode=true \
   --browser-name android-chrome --android-emulator \
   --flavor github
 ```
 
 To ues the emulator `--browser-name android-chrome --android-emulator` is 
-required. `--flavor github` is needed for the driver to find the apk. All tests are run by the CI and can also be manually run before merge:
+required. `--flavor github` is needed for the driver to find the apk. `--dart-define=testing_mode=true` is needed to avoid some caching that messes with tests. 
 
 ```bash
 flutter test integration_test --flavor github
fastlane/metadata/android/en-US/images/phoneScreenshots/01-example_add.png
Binary file
fastlane/metadata/android/en-US/images/phoneScreenshots/02-example_home.png
Binary file
fastlane/metadata/android/en-US/images/phoneScreenshots/03-example_settings.png
Binary file
fastlane/metadata/android/en-US/images/phoneScreenshots/04-example_stats.png
Binary file
update-screenshots.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+OUT_DIR="fastlane/metadata/android/en-US/images/phoneScreenshots"
+
+cd app || exit 1
+flutter drive --target=integration_test/screenshot_home.dart --dart-define=testing_mode=true --driver=test_driver/integration_test.dart  --browser-name android-chrome --android-emulator --flavor github --no-cache-startup-profile --no-enable-dart-profiling --no-track-widget-creation || exit 1
+flutter drive --target=integration_test/screenshot_input.dart --dart-define=testing_mode=true --driver=test_driver/integration_test.dart  --browser-name android-chrome --android-emulator --flavor github --no-cache-startup-profile --no-enable-dart-profiling --no-track-widget-creation --no-pub || exit 1
+flutter drive --target=integration_test/screenshot_settings.dart --dart-define=testing_mode=true --driver=test_driver/integration_test.dart  --browser-name android-chrome --android-emulator --flavor github --no-cache-startup-profile --no-enable-dart-profiling --no-track-widget-creation --no-pub || exit 1
+flutter drive --target=integration_test/screenshot_stats.dart --dart-define=testing_mode=true --driver=test_driver/integration_test.dart  --browser-name android-chrome --android-emulator --flavor github --no-cache-startup-profile --no-enable-dart-profiling --no-track-widget-creation --no-pub || exit 1
+
+cd build/screenshots
+# remove top 80 px and resize to 2:1 ratio
+find . -maxdepth 1 -iname "*.png" | xargs -L1 -I{} magick "{}" -crop 1080x2074+0+80 +repage -resize 1000x2000! "../../../$OUT_DIR/{}"
\ No newline at end of file