Commit 31ec21a
Changed files (14)
app
docs
fastlane
metadata
android
en-US
images
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/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