Commit d77d831

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-06-12 11:32:29
Allow adding further integration tests
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 7ff91d8
Changed files (9)
app/integration_test/add_measurement_test.dart
@@ -1,18 +1,24 @@
+import 'package:blood_pressure_app/app.dart';
 import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
 import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
-import 'package:blood_pressure_app/main.dart' as app;
+import 'package:blood_pressure_app/components/settings/color_picker_list_tile.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';
 
-void main() {
-  final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+import '../test/ui/components/settings/color_picker_list_tile_test.dart';
+import 'util.dart';
 
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
   testWidgets('Can enter value only measurements', (WidgetTester tester) async {
+    await tester.pumpWidget(App(forceClearAppDataOnLaunch: true));
     final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-    app.main();
     await tester.pumpAndSettle();
+    await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+    expect(find.byType(AppHome), findsOneWidget);
     expect(find.byType(AddEntryDialoge), findsNothing);
     expect(find.byType(MeasurementListRow), findsNothing);
 
@@ -24,18 +30,13 @@ void main() {
     await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
     await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
     await tester.enterText(find.byType(TextFormField).at(2), '56'); // pul
-    
+
     await tester.tap(find.text(localizations.btnSave));
     await tester.pumpAndSettle();
     expect(find.byType(AddEntryDialoge), findsNothing);
 
-    // Gets up to 5s to load from fs.
-    int retries = 10;
-    while(find.text(localizations.loading).hasFound && retries >= 0) {
-      retries--;
-      await tester.pump(Duration(milliseconds: 500));
-    }
-    await tester.pump();
+    await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
+    expect(find.text(localizations.loading), findsNothing);
 
     expect(find.byType(MeasurementListRow), findsOneWidget);
     expect(find.descendant(
@@ -52,4 +53,45 @@ void main() {
     ), findsOneWidget,);
 
   });
+
+  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.pumpAndSettle();
+    await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+    expect(find.byType(AppHome), findsOneWidget);
+
+    await tester.tap(find.byIcon(Icons.add));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialoge), findsOneWidget);
+
+    await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
+    await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
+    await tester.enterText(find.byType(TextFormField).at(2), '56'); // pul
+    await tester.enterText(find.byType(TextFormField).at(3), 'some test sample note'); // note
+    await tester.tap(find.byType(ColorSelectionListTile));
+    await tester.pumpAndSettle();
+    await tester.tap(find.byElementPredicate(findColored(Colors.red)));
+    await tester.pumpAndSettle();
+
+    await tester.tap(find.text(localizations.btnSave));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialoge), findsNothing);
+
+    await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
+    expect(find.text(localizations.loading), findsNothing);
+
+    expect(find.byType(MeasurementListRow), findsOneWidget);
+    final submittedRecord = tester.widget<MeasurementListRow>(find.byType(MeasurementListRow)).record;
+    expect(submittedRecord.systolic, 123);
+    expect(submittedRecord.diastolic, 67);
+    expect(submittedRecord.pulse, 56);
+    expect(submittedRecord.needlePin?.color.value, Colors.red.value);
+    expect(submittedRecord.notes, 'some test sample note');
+
+    expect(find.text('some test sample note'), findsNothing);
+    await tester.tap(find.byType(MeasurementListRow));
+    await tester.pumpAndSettle();
+    expect(find.text('some test sample note'), findsOneWidget);
+  });
 }
app/integration_test/util.dart
@@ -0,0 +1,17 @@
+import 'package:flutter_test/flutter_test.dart';
+
+extension WaitUntil on WidgetTester {
+  /// Retries with 100ms delay for up to [maxLength] for a [test] to succeed.
+  ///
+  /// When no value is provided [maxLength] defaults to 5s.
+  Future<void> pumpUntil(bool Function() test, [Duration? maxLength]) async {
+    maxLength ??= Duration(seconds: 5);
+
+    int retries = maxLength.inMilliseconds ~/ 100;
+    while(!test() && retries >= 0) {
+      retries--;
+      await pump(Duration(milliseconds: 100));
+    }
+    await pump();
+  }
+}
\ No newline at end of file
app/lib/model/blood_pressure/model.dart
@@ -21,11 +21,9 @@ class BloodPressureModel extends ChangeNotifier {
 
   Future<void> _asyncInit(String? dbPath, bool isFullPath) async {
     dbPath ??= await getDatabasesPath();
-
     if (dbPath != inMemoryDatabasePath && !isFullPath) {
       dbPath = join(dbPath, 'blood_pressure.db');
     }
-
     // In case safer data loading is needed: finish this.
     /*
     String? backupPath;
@@ -44,6 +42,8 @@ class BloodPressureModel extends ChangeNotifier {
       onUpgrade: _onDBUpgrade,
       // When increasing the version an update procedure from every other possible version is needed
       version: 2,
+      // In integration tests the file may be deleted which causes deadlocks.
+      singleInstance: false,
     );
   }
 
@@ -58,8 +58,8 @@ class BloodPressureModel extends ChangeNotifier {
     // When adding more versions the upgrade procedure proposed in https://stackoverflow.com/a/75153875/21489239
     // might be useful, to avoid duplicated code. Currently this would only lead to complexity, without benefits.
     if (oldVersion == 1 && newVersion == 2) {
-      db.execute('ALTER TABLE bloodPressureModel ADD COLUMN needlePin STRING;');
-      db.database.setVersion(2);
+      await db.execute('ALTER TABLE bloodPressureModel ADD COLUMN needlePin STRING;');
+      await db.database.setVersion(2);
     } else {
       await ErrorReporting.reportCriticalError('Unsupported database upgrade', 'Attempted to upgrade the measurement database from version $oldVersion to version $newVersion, which is not supported. This action failed to avoid data loss. Please contact the app developer by opening an issue with the link below or writing an email to contact@derdilla.com.');
     }
app/lib/model/storage/db/config_dao.dart
@@ -65,12 +65,12 @@ class ConfigDao {
   Future<void> _updateSettings(int profileID, Settings settings) async {
     if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
-        ConfigDB.settingsTable,
-        {
-          'profile_id': profileID,
-          'settings_json': settings.toJson(),
-        },
-        conflictAlgorithm: ConflictAlgorithm.replace,
+      ConfigDB.settingsTable,
+      {
+        'profile_id': profileID,
+        'settings_json': settings.toJson(),
+      },
+      conflictAlgorithm: ConflictAlgorithm.replace,
     );
   }
 
app/lib/model/storage/db/config_db.dart
@@ -105,6 +105,8 @@ class ConfigDB {
       onUpgrade: _onDBUpgrade,
       // When increasing the version an update procedure from every other possible version is needed
       version: 3,
+      // In integration tests the file may be deleted which causes deadlocks.
+      singleInstance: false,
     );
   }
 
app/lib/app.dart
@@ -60,19 +60,19 @@ class _AppState extends State<App> {
     WidgetsFlutterBinding.ensureInitialized();
 
     if (widget.forceClearAppDataOnLaunch) {
+      final dbPath = await getDatabasesPath();
       try {
-        final dbPath = await getDatabasesPath();
-        File(join(await getDatabasesPath(), 'blood_pressure.db')).deleteSync();
-        File(join(await getDatabasesPath(), 'blood_pressure.db-journal')).deleteSync();
+        File(join(dbPath, 'blood_pressure.db')).deleteSync();
+        File(join(dbPath, 'blood_pressure.db-journal')).deleteSync();
       } on FileSystemException {
         // File is likely already deleted or couldn't be created in the first place.
       }
       try {
-        File(join(await getDatabasesPath(), 'config.db')).deleteSync();
-        File(join(await getDatabasesPath(), 'config.db-journal')).deleteSync();
+        File(join(dbPath, 'config.db')).deleteSync();
+        File(join(dbPath, 'config.db-journal')).deleteSync();
       } on FileSystemException { }
       try {
-        File(join(await getDatabasesPath(), 'medicine.intakes')).deleteSync();
+        File(join(dbPath, 'medicine.intakes')).deleteSync();
       } on FileSystemException { }
     }
 
app/test/ui/components/add_measurement_dialoge_test.dart
@@ -449,7 +449,6 @@ void main() {
       expect(thirdFocusedTextFormField.evaluate().first.widget, isA<TextFormField>()
           .having((p0) => p0.initialValue, 'note input content', 'note'),);
     });
-
     testWidgets('should focus last input field on backspace pressed in empty input field', (tester) async {
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(), mockRecord(sys: 12, dia: 3, pul: 4, note: 'note')),);
app/test/ui/navigation_test.dart
@@ -1,116 +1,2 @@
-import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
-import 'package:blood_pressure_app/components/dialoges/enter_timeformat_dialoge.dart';
-import 'package:blood_pressure_app/main.dart';
-import 'package:blood_pressure_app/model/blood_pressure/medicine/intake_history.dart';
-import 'package:blood_pressure_app/model/blood_pressure/model.dart';
-import 'package:blood_pressure_app/model/storage/db/config_dao.dart';
-import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
-import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/intervall_store.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/screens/settings_screen.dart';
-import 'package:blood_pressure_app/screens/statistics_screen.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:provider/provider.dart';
 
-import '../ram_only_implementations.dart';
-
-void main() {
-  group('start page', () {
-    testWidgets('should navigate to add entry page', (tester) async {
-      await pumpAppRoot(tester);
-      expect(find.byIcon(Icons.add), findsOneWidget);
-      await tester.tap(find.byIcon(Icons.add));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-    });
-    testWidgets('should navigate to settings page', (tester) async {
-      await pumpAppRoot(tester);
-      expect(find.byIcon(Icons.settings), findsOneWidget);
-      await tester.tap(find.byIcon(Icons.settings));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(SettingsPage), findsOneWidget);
-    });
-    testWidgets('should navigate to stats page', (tester) async {
-      await pumpAppRoot(tester);
-      expect(find.byIcon(Icons.insights), findsOneWidget);
-      await tester.tap(find.byIcon(Icons.insights));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(StatisticsScreen), findsOneWidget);
-    });
-  });
-  group('settings page', () {
-    testWidgets('open EnterTimeFormatScreen', (tester) async {
-      await pumpAppRoot(tester);
-      expect(find.byIcon(Icons.settings), findsOneWidget);
-      await tester.tap(find.byIcon(Icons.settings));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(SettingsPage), findsOneWidget);
-      expect(find.byType(EnterTimeFormatDialoge), findsNothing);
-      expect(find.byKey(const Key('EnterTimeFormatScreen')), findsOneWidget);
-      await tester.tap(find.byKey(const Key('EnterTimeFormatScreen')));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(EnterTimeFormatDialoge), findsOneWidget);
-    });
-    // ...
-  });
-}
-
-/// Creates a the same App as the main method.
-Future<void> pumpAppRoot(WidgetTester tester, {
-  Settings? settings,
-  ExportSettings? exportSettings,
-  CsvExportSettings? csvExportSettings,
-  PdfExportSettings? pdfExportSettings,
-  IntervallStoreManager? intervallStoreManager,
-  IntakeHistory? intakeHistory,
-  BloodPressureModel? model,
-}) async {
-  model ??= RamBloodPressureModel();
-  settings ??= Settings();
-  exportSettings ??= ExportSettings();
-  csvExportSettings ??= CsvExportSettings();
-  pdfExportSettings ??= PdfExportSettings();
-  intakeHistory ??= IntakeHistory([]);
-  intervallStoreManager ??= IntervallStoreManager(IntervallStorage(), IntervallStorage(), IntervallStorage());
-
-  await tester.pumpWidget(MultiProvider(providers: [
-    ChangeNotifierProvider(create: (_) => settings),
-    ChangeNotifierProvider(create: (_) => exportSettings),
-    ChangeNotifierProvider(create: (_) => csvExportSettings),
-    ChangeNotifierProvider(create: (_) => pdfExportSettings),
-    ChangeNotifierProvider(create: (_) => intakeHistory),
-    ChangeNotifierProvider(create: (_) => intervallStoreManager),
-    ChangeNotifierProvider<BloodPressureModel>(create: (_) => model!),
-  ], child: const AppRoot(),),);
-}
-
-class MockConfigDao implements ConfigDao {
-  @override
-  Future<CsvExportSettings> loadCsvExportSettings(int profileID) async => CsvExportSettings();
-
-  @override
-  Future<ExportSettings> loadExportSettings(int profileID) async => ExportSettings();
-
-  @override
-  Future<IntervallStorage> loadIntervallStorage(int profileID, int storageID) async => IntervallStorage();
-
-  @override
-  Future<PdfExportSettings> loadPdfExportSettings(int profileID) async => PdfExportSettings();
-
-  @override
-  Future<Settings> loadSettings(int profileID) async => Settings();
-
-  void reset() {}
-
-  @override
-  Future<ExportColumnsManager> loadExportColumnsManager(int profileID) async => ExportColumnsManager();
-}
+// TODO: navigation tests as integration tests
docs/testing.md
@@ -4,13 +4,28 @@ Testing means catching bugs early and automated testing has already prevented
 multiple bugs from getting reintroduced. Therefor the goal is to have the 
 entire codebase covered by extensive tests.
 
-#### Running unit tests
+Integration
+
+### Unit tests
+
+Unit tests are fast and can all be run during development. Some util functions 
+are present in the util file and in specialised ones in their respective widget
+test (e.g. color picker).
 
 ```bash
 flutter test
 ```
 
-#### Running integration tests
+#### Integration test
+
+Integration tests are slow and mainly used for core workflows and things that 
+can't be tested without them. Integration tests should not use the `main` 
+method but should rather pump the App directly to allow tests to be independent
+of each other.
+
+```dart
+tester.pumpWidget(App(forceClearAppDataOnLaunch: true,));
+```
 
 To run integration tests an android emulator needs to be running.