Commit a2e2a8c

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-04-16 20:33:03
mock med repo in tests to avoid deadlocks (16 failing)
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 2449360
Changed files (4)
app/lib/components/dialoges/add_measurement_dialoge.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:math';
 
+import 'package:blood_pressure_app/components/consistent_future_builder.dart';
 import 'package:blood_pressure_app/components/date_time_picker.dart';
 import 'package:blood_pressure_app/components/dialoges/fullscreen_dialoge.dart';
 import 'package:blood_pressure_app/components/settings/settings_widgets.dart';
@@ -53,11 +54,6 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
   final noteFocusNode = FocusNode();
   late final TextEditingController sysController;
 
-  /// List of medicines.
-  ///
-  /// Filled after fetching from repo is complete.
-  List<Medicine> availableMeds = [];
-
   /// Currently selected time.
   late DateTime time;
 
@@ -110,9 +106,6 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
       _measurementFormActive = true;
     }
 
-    unawaited(widget.medRepo.getAll()
-        .then((value) => setState(() => availableMeds.addAll(value))));
-
     sysFocusNode.requestFocus();
     ServicesBinding.instance.keyboard.addHandler(_onKey);
   }
@@ -355,72 +348,75 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
                 ],
               ),
             ),
-            if (availableMeds.isNotEmpty && widget.initialRecord == null)
-              Form(
-                key: medicationFormKey,
-                child: Padding(
-                  padding: const EdgeInsets.symmetric(vertical: 16),
-                  child: Row(
-                    children: [
-                      Expanded(
-                        child: DropdownButtonFormField<Medicine?>(
-                          isExpanded: true,
-                          value: selectedMed,
-                          items: [
-                            for (final e in availableMeds)
+            if (widget.initialRecord == null)
+              ConsistentFutureBuilder(
+                future: widget.medRepo.getAll(),
+                onData: (BuildContext context, List<Medicine> availableMeds) => Form(
+                  key: medicationFormKey,
+                  child: Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 16),
+                    child: Row(
+                      children: [
+                        Expanded(
+                          child: DropdownButtonFormField<Medicine?>(
+                            isExpanded: true,
+                            value: selectedMed,
+                            items: [
+                              for (final e in availableMeds)
+                                DropdownMenuItem(
+                                  value: e,
+                                  child: Text(e.designation),
+                                ),
                               DropdownMenuItem(
-                                value: e,
-                                child: Text(e.designation),
+                                child: Text(localizations.noMedication),
                               ),
-                            DropdownMenuItem(
-                              child: Text(localizations.noMedication),
-                            ),
-                          ],
-                          onChanged: (v) {
-                            setState(() {
-                              if (v != null) {
-                                _showMedicineDosisInput = true;
-                                selectedMed = v;
-                                medicineDosis = v.dosis?.mg;
-                              } else {
-                                _showMedicineDosisInput = false;
-                                selectedMed = null;
-                              }
-                            });
-                          },
-                        ),
-                      ),
-                      if (_showMedicineDosisInput)
-                        const SizedBox(width: 16,),
-                      if (_showMedicineDosisInput)
-                        Expanded(
-                          child: TextFormField(
-                            initialValue: medicineDosis?.toString(),
-                            decoration: InputDecoration(
-                              labelText: localizations.dosis,
-                            ),
-                            keyboardType: TextInputType.number,
-                            onChanged: (value) {
+                            ],
+                            onChanged: (v) {
                               setState(() {
-                                final dosis = int.tryParse(value)?.toDouble()
-                                    ?? double.tryParse(value);
-                                if(dosis != null && dosis > 0) medicineDosis = dosis;
+                                if (v != null) {
+                                  _showMedicineDosisInput = true;
+                                  selectedMed = v;
+                                  medicineDosis = v.dosis?.mg;
+                                } else {
+                                  _showMedicineDosisInput = false;
+                                  selectedMed = null;
+                                }
                               });
                             },
-                            inputFormatters: [FilteringTextInputFormatter.allow(
-                              RegExp(r'([0-9]+(\.([0-9]*))?)'),),],
-                            validator: (String? value) {
-                              if (!_showMedicineDosisInput) return null;
-                              if (((int.tryParse(value ?? '')?.toDouble()
-                                  ?? double.tryParse(value ?? '')) ?? 0) <= 0) {
-                                return localizations.errNaN;
-                              }
-                              return null;
-                            },
                           ),
                         ),
+                        if (_showMedicineDosisInput)
+                          const SizedBox(width: 16,),
+                        if (_showMedicineDosisInput)
+                          Expanded(
+                            child: TextFormField(
+                              initialValue: medicineDosis?.toString(),
+                              decoration: InputDecoration(
+                                labelText: localizations.dosis,
+                              ),
+                              keyboardType: TextInputType.number,
+                              onChanged: (value) {
+                                setState(() {
+                                  final dosis = int.tryParse(value)?.toDouble()
+                                      ?? double.tryParse(value);
+                                  if(dosis != null && dosis > 0) medicineDosis = dosis;
+                                });
+                              },
+                              inputFormatters: [FilteringTextInputFormatter.allow(
+                                RegExp(r'([0-9]+(\.([0-9]*))?)'),),],
+                              validator: (String? value) {
+                                if (!_showMedicineDosisInput) return null;
+                                if (((int.tryParse(value ?? '')?.toDouble()
+                                    ?? double.tryParse(value ?? '')) ?? 0) <= 0) {
+                                  return localizations.errNaN;
+                                }
+                                return null;
+                              },
+                            ),
+                          ),
 
-                    ],
+                      ],
+                    ),
                   ),
                 ),
               ),
app/test/ui/components/add_measurement_dialoge_test.dart
@@ -1,6 +1,5 @@
 import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
 import 'package:blood_pressure_app/components/settings/color_picker_list_tile.dart';
-import 'package:blood_pressure_app/model/blood_pressure/medicine/medicine_intake.dart';
 import 'package:blood_pressure_app/model/blood_pressure/needle_pin.dart';
 import 'package:blood_pressure_app/model/blood_pressure/record.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
@@ -17,9 +16,10 @@ import 'util.dart';
 void main() {
   group('AddEntryDialoge', () {
     testWidgets('should show everything on initial page', (tester) async {
+      final repo = medRepo();
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
-          medRepo: await medRepo(),
+          medRepo: repo,
           settings: Settings(),
         ),
       ),);
@@ -32,7 +32,7 @@ void main() {
       expect(find.text('Diastolic'), findsWidgets);
       expect(find.text('Pulse'), findsWidgets);
       expect(find.byType(ColorSelectionListTile), findsOneWidget);
-    }, timeout: const Timeout(Duration(seconds: 10)),);
+    },);
     testWidgets('should prefill initialRecord values', (tester) async {
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
@@ -41,7 +41,7 @@ void main() {
             DateTime.now(), 123, 56, 43, 'Test note',
             needlePin: const MeasurementNeedlePin(Colors.teal),
           ),
-          medRepo: await medRepo(),
+          medRepo: medRepo(),
         ),
       ),);
       await tester.pumpAndSettle();
@@ -60,7 +60,7 @@ void main() {
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
           settings: Settings(),
-          medRepo: await medRepo([mockMedicine(designation: 'testmed')]),
+          medRepo: medRepo([mockMedicine(designation: 'testmed')]),
         ),
       ),);
       await tester.pumpAndSettle();
@@ -79,7 +79,7 @@ void main() {
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
           settings: Settings(),
-          medRepo: await medRepo([mockMedicine(designation: 'testmed')]),
+          medRepo: medRepo([mockMedicine(designation: 'testmed')]),
         ),
       ),);
       await tester.pumpAndSettle();
@@ -109,7 +109,7 @@ void main() {
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
           settings: Settings(),
-          medRepo: await medRepo([mockMedicine(designation: 'testmed', defaultDosis: 3.1415)]),
+          medRepo: medRepo([mockMedicine(designation: 'testmed', defaultDosis: 3.1415)]),
         ),
       ),);
       await tester.pumpAndSettle();
@@ -126,7 +126,7 @@ void main() {
       await tester.pumpWidget(materialApp(
         AddEntryDialoge(
           settings: Settings(),
-          medRepo: await medRepo([mockMedicine(designation: 'testmed', defaultDosis: 3.1415)]),
+          medRepo: medRepo([mockMedicine(designation: 'testmed', defaultDosis: 3.1415)]),
         ),
       ),);
       await tester.pumpAndSettle();
@@ -158,7 +158,7 @@ void main() {
       dynamic result = 'result before save';
       await loadDialoge(tester, (context) async
       => result = await showAddEntryDialoge(context, Settings(),
-        await medRepo(),
+        medRepo(),
         mockRecord(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal),),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
 
@@ -173,7 +173,7 @@ void main() {
       dynamic result = 'result before save';
       final record = mockRecord(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal);
       await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, Settings(), await medRepo(), record),);
+      => result = await showAddEntryDialoge(context, Settings(), medRepo(), record),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
 
       expect(find.byType(AddEntryDialoge), findsOneWidget);
@@ -190,7 +190,7 @@ void main() {
     testWidgets('should be able to input records', (WidgetTester tester) async {
       dynamic result = 'result before save';
       await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, Settings(), await medRepo(),),);
+      => result = await showAddEntryDialoge(context, Settings(), medRepo(),),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
 
       await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
@@ -219,7 +219,7 @@ void main() {
     testWidgets('should allow value only', (WidgetTester tester) async {
       dynamic result = 'result before save';
       await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, Settings(), await medRepo(),),);
+      => result = await showAddEntryDialoge(context, Settings(), medRepo(),),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
       final localizations = await AppLocalizations.delegate.load(const Locale('en'));
 
@@ -249,7 +249,7 @@ void main() {
       => result = await showAddEntryDialoge(context, Settings(
         allowMissingValues: true,
         validateInputs: false,
-      ), await medRepo(),),);
+      ), medRepo(),),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
 
       final localizations = await AppLocalizations.delegate.load(const Locale('en'));
@@ -274,7 +274,7 @@ void main() {
 
       dynamic result = 'result before save';
       await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, Settings(), await medRepo([
+      => result = await showAddEntryDialoge(context, Settings(), medRepo([
         mockMedicine(designation: 'medication1'),
         med2,
       ],),),);
@@ -302,14 +302,14 @@ void main() {
       await tester.pumpAndSettle();
 
       expect(result?.$1, isNull);
-      expect(result?.$2, isA<OldMedicineIntake>()
-          .having((p0) => p0.timestamp.millisecondsSinceEpoch ~/ 2000, 'timestamp', openDialogeTimeStamp.millisecondsSinceEpoch ~/ 2000)
+      expect(result?.$2, isA<MedicineIntake>()
+          .having((p0) => p0.time.millisecondsSinceEpoch ~/ 2000, 'timestamp', openDialogeTimeStamp.millisecondsSinceEpoch ~/ 2000)
           .having((p0) => p0.medicine, 'medicine', med2)
           .having((p0) => p0.dosis, 'dosis', 123.456),
       );
     });
     testWidgets('should not allow invalid values', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) => showAddEntryDialoge(context, Settings(), mRep));
       final localizations = await AppLocalizations.delegate.load(const Locale('en'));
 
@@ -373,7 +373,7 @@ void main() {
       expect(find.text(localizations.errDiaGtSys), findsNothing);
     });
     testWidgets('should allow invalid values when setting is set', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(validateInputs: false, allowMissingValues: true), mRep),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -385,7 +385,7 @@ void main() {
       expect(find.byType(AddEntryDialoge), findsNothing);
     });
     testWidgets('should respect settings.allowManualTimeInput', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(allowManualTimeInput: false), mRep),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -393,7 +393,7 @@ void main() {
       expect(find.byIcon(Icons.edit), findsNothing);
     });
     testWidgets('should start with sys input focused', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(), mRep, mockRecord(sys: 12)),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -409,7 +409,7 @@ void main() {
           .having((p0) => p0.initialValue, 'systolic content', '12'),);
     });
     testWidgets('should focus next on input finished', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(), mRep, mockRecord(sys: 12, dia: 3, pul: 4, note: 'note')),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -452,7 +452,7 @@ void main() {
     });
 
     testWidgets('should focus last input field on backspace pressed in empty input field', (tester) async {
-      final mRep = await medRepo();
+      final mRep = medRepo();
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(), mRep, mockRecord(sys: 12, dia: 3, pul: 4, note: 'note')),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -513,7 +513,7 @@ void main() {
       expect(find.descendant(of: fourthFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
     });
     testWidgets('should allow entering custom dosis', (tester) async {
-      final mRep = await medRepo([mockMedicine(designation: 'testmed')]);
+      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
       dynamic result;
       await loadDialoge(tester, (context) async =>
         result = await showAddEntryDialoge(context, Settings(), mRep),
@@ -543,12 +543,12 @@ void main() {
 
       expect(result, isNotNull);
       expect(result?.$1, isNull);
-      expect(result?.$2, isA<OldMedicineIntake>()
+      expect(result?.$2, isA<MedicineIntake>()
           .having((p0) => p0.dosis, 'dosis', 654.321),
       );
     });
     testWidgets('should allow modifying entered dosis', (tester) async {
-      final mRep = await medRepo([mockMedicine(designation: 'testmed')]);
+      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
       dynamic result;
       await loadDialoge(tester, (context) async =>
         result = await showAddEntryDialoge(context, Settings(), mRep),
@@ -583,12 +583,12 @@ void main() {
 
       expect(result, isNotNull);
       expect(result?.$1, isNull);
-      expect(result?.$2, isA<OldMedicineIntake>()
+      expect(result?.$2, isA<MedicineIntake>()
           .having((p0) => p0.dosis, 'dosis', 654.322),
       );
     });
     testWidgets('should not go back to last field when the current field is still filled', (tester) async {
-      final mRep = await medRepo([mockMedicine(designation: 'testmed')]);
+      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
       await loadDialoge(tester, (context) =>
           showAddEntryDialoge(context, Settings(), mRep, mockRecord(sys: 12, dia: 3, pul: 4, note: 'note')),);
       expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
@@ -608,6 +608,6 @@ void main() {
       expect(focusedTextFormField, findsOneWidget);
       expect(find.descendant(of: focusedTextFormField, matching: find.text('Pulse')), findsNothing);
       expect(find.descendant(of: focusedTextFormField, matching: find.text('Note (optional)')), findsWidgets);
-    }, timeout: Timeout(Duration(seconds: 30)));
+    });
   });
 }
app/test/ui/components/util.dart
@@ -9,6 +9,8 @@ import 'package:health_data_store/health_data_store.dart';
 import 'package:provider/provider.dart';
 import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
+import '../../ram_only_implementations.dart';
+
 /// Create a root material widget with localizations.
 Widget materialApp(Widget child) => MaterialApp(
   localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -53,7 +55,8 @@ Future<Widget> appBase(Widget child, {
     ));
   }
 
-  return Provider(create: (_) => model,
+  return Provider<BloodPressureModel>(
+    create: (_) => model ?? RamBloodPressureModel(),
     child: await newAppBase(child,
       settings: settings,
       exportSettings: exportSettings,
@@ -123,15 +126,20 @@ Future<void> loadDialoge(WidgetTester tester, void Function(BuildContext context
 }
 
 /// Get empty mock med repo.
-Future<MedicineRepository>  medRepo([List<Medicine>? meds]) async  {
-  final db = await _getHealthDateStore();
-  final repo = db.medRepo;
-  if (meds != null) {
-    for (final med in meds) {
-      await repo.add(med);
-    }
-  }
-  return repo;
+// Using a instance of the real repository somehow causes a deadlock in tests.
+MedicineRepository medRepo([List<Medicine>? meds]) => MockMedRepo();
+
+class MockMedRepo implements MedicineRepository {
+  final List<Medicine> _meds = [];
+
+  @override
+  Future<void> add(Medicine medicine) async => _meds.add(medicine);
+
+  @override
+  Future<List<Medicine>> getAll() async=> _meds;
+
+  @override
+  Future<void> remove(Medicine value) async => _meds.remove(value);
 }
 
 final List<Medicine> _meds = [];
app/analysis_options.yaml
@@ -10,6 +10,7 @@ linter:
     - comment_references
     - no_wildcard_variable_uses
     - prefer_void_to_null
+    - unawaited_futures
 
     # Style:
     - always_declare_return_types