Commit a2e2a8c
Changed files (4)
app
lib
components
dialoges
test
ui
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