Commit 2456a80
Changed files (32)
app
integration_test
lib
data_util
features
input
old_bluetooth
l10n
model
storage
test
health_data_store
lib
src
repositories
app/integration_test/add_measurement_test.dart
@@ -1,5 +1,5 @@
import 'package:blood_pressure_app/app.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
import 'package:blood_pressure_app/features/measurement_list/measurement_list_entry.dart';
import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
import 'package:blood_pressure_app/screens/home_screen.dart';
@@ -20,13 +20,13 @@ void main() {
await tester.pumpAndSettle();
await tester.pumpUntil(() => find.byType(AppHome).hasFound);
expect(find.byType(AppHome), findsOneWidget);
- expect(find.byType(AddEntryDialoge), findsNothing);
+ expect(find.byType(AddEntryDialogue), findsNothing);
expect(find.byType(MeasurementListRow), findsNothing);
expect(find.byIcon(Icons.add), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
@@ -34,7 +34,7 @@ void main() {
await tester.tap(find.text(localizations.btnSave));
await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
+ expect(find.byType(AddEntryDialogue), findsNothing);
await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
expect(find.text(localizations.loading), findsNothing);
@@ -64,7 +64,7 @@ void main() {
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
@@ -77,7 +77,7 @@ void main() {
await tester.tap(find.text(localizations.btnSave));
await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
+ expect(find.byType(AddEntryDialogue), findsNothing);
await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
expect(find.text(localizations.loading), findsNothing);
app/lib/data_util/entry_context.dart
@@ -1,6 +1,7 @@
import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
import 'package:blood_pressure_app/features/export_import/export_button.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
import 'package:blood_pressure_app/logging.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:blood_pressure_app/screens/error_reporting_screen.dart';
@@ -12,43 +13,33 @@ import 'package:provider/provider.dart';
/// Allow high level operations on the repositories in context.
extension EntryUtils on BuildContext {
- /// Open the [AddEntryDialoge] and save received entries.
+ /// Open the [AddEntryDialogue] and save received entries.
///
/// Follows [ExportSettings.exportAfterEveryEntry]. When [initial] is not null
/// the dialoge will be opened in edit mode.
- Future<void> createEntry([FullEntry? initial]) async {
+ Future<void> createEntry([AddEntryFormValue? initial]) async {
try {
final recordRepo = RepositoryProvider.of<BloodPressureRepository>(this);
final noteRepo = RepositoryProvider.of<NoteRepository>(this);
final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(this);
+ final weightRepo = RepositoryProvider.of<BodyweightRepository>(this);
final exportSettings = Provider.of<ExportSettings>(this, listen: false);
- final entry = await showAddEntryDialoge(this,
+ final entry = await showAddEntryDialogue(this,
RepositoryProvider.of<MedicineRepository>(this),
initial,
);
if (entry != null) {
- if (initial != null) {
- if ((initial.sys != null || initial.dia != null || initial.pul != null)) {
- await recordRepo.remove(initial.$1);
- }
- if ((initial.note != null || initial.color != null)) {
- await noteRepo.remove(initial.$2);
- }
- for (final intake in initial.$3) {
- await intakeRepo.remove(intake);
- }
- }
+ if (initial?.record != null) await recordRepo.remove(initial!.record!);
+ if (initial?.note != null) await noteRepo.remove(initial!.note!);
+ if (initial?.intake != null) await intakeRepo.remove(initial!.intake!);
+ if (initial?.weight != null) await weightRepo.remove(initial!.weight!);
+
+ if (entry.record != null) await recordRepo.add(entry.record!);
+ if (entry.note != null) await noteRepo.add(entry.note!);
+ if (entry.intake != null) await intakeRepo.add(entry.intake!);
+ if(entry.weight != null) await weightRepo.add(entry.weight!);
- if (entry.sys != null || entry.dia != null || entry.pul != null) {
- await recordRepo.add(entry.$1);
- }
- if (entry.note != null || entry.color != null) {
- await noteRepo.add(entry.$2);
- }
- for (final intake in entry.$3) {
- await intakeRepo.add(intake);
- }
if (mounted && exportSettings.exportAfterEveryEntry) {
read<IntervalStoreManager>().exportPage.setToMostRecentInterval();
performExport(this);
app/lib/features/input/forms/add_entry_form.dart
@@ -0,0 +1,298 @@
+import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
+import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/ble_read_cubit.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/device_scan_cubit.dart';
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/features/input/forms/form_switcher.dart';
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/logging.dart';
+import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
+
+/// Primary form to enter all types of entries.
+class AddEntryForm extends FormBase<AddEntryFormValue> with TypeLogger {
+ /// Create primary form to enter all types of entries.
+ const AddEntryForm({super.key,
+ super.initialValue,
+ this.meds = const [],
+ this.bluetoothCubit,
+ });
+
+ /// All medicines selectable.
+ ///
+ /// Hides med input when this is empty.
+ final List<Medicine> meds;
+
+ /// Function to customize [BluetoothCubit] creation.
+ @visibleForTesting
+ final BluetoothCubit Function()? bluetoothCubit;
+
+ @override
+ FormStateBase createState() => AddEntryFormState();
+}
+
+/// State of primary form to enter all types of entries.
+class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
+ final _timeForm = GlobalKey<DateTimeFormState>();
+ final _noteForm = GlobalKey<NoteFormState>();
+ final _bpForm = GlobalKey<BloodPressureFormState>();
+ final _weightForm = GlobalKey<WeightFormState>();
+ final _intakeForm = GlobalKey<MedicineIntakeFormState>();
+
+ final _controller = FormSwitcherController();
+
+ // because these values are no necessarily in tree a copy is needed to get
+ // overridden values.
+ BloodPressureRecord? _lastSavedPressure;
+ BodyweightRecord? _lastSavedWeight;
+ MedicineIntake? _lastSavedIntake;
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.initialValue != null) {
+ _lastSavedPressure = widget.initialValue?.record;
+ _lastSavedWeight = widget.initialValue?.weight;
+ _lastSavedIntake = widget.initialValue?.intake;
+ if (widget.initialValue!.record == null
+ && widget.initialValue!.intake == null
+ && widget.initialValue!.weight != null) {
+ _controller.animateTo(2);
+ } else if (widget.initialValue!.record == null
+ && widget.initialValue!.intake != null) {
+ _controller.animateTo(1);
+ }
+ // In all other cases we are at the correct position (0)
+ // or don't need to jump at all.
+ }
+ ServicesBinding.instance.keyboard.addHandler(_onKey);
+ }
+
+ @override
+ void dispose() {
+ ServicesBinding.instance.keyboard.removeHandler(_onKey);
+ super.dispose();
+ }
+
+ bool _onKey(KeyEvent event) {
+ if(event.logicalKey == LogicalKeyboardKey.backspace
+ && ((_bpForm.currentState?.isEmptyInputFocused() ?? false)
+ || (_noteForm.currentState?.isEmptyInputFocused() ?? false)
+ || (_weightForm.currentState?.isEmptyInputFocused() ?? false)
+ || (_intakeForm.currentState?.isEmptyInputFocused() ?? false))) {
+ FocusScope.of(context).previousFocus();
+ }
+ return false;
+ }
+
+ @override
+ bool validate() => !context.read<Settings>().validateInputs
+ || (_timeForm.currentState?.validate() ?? false)
+ && (_noteForm.currentState?.validate() ?? false)
+ // the following become null when unopened
+ && (_bpForm.currentState?.validate() ?? true)
+ && (_weightForm.currentState?.validate() ?? true)
+ && (_intakeForm.currentState?.validate() ?? true);
+
+ @override
+ AddEntryFormValue? save() {
+ if (!validate()) return null;
+ final time = _timeForm.currentState!.save()!;
+ Note? note;
+ BloodPressureRecord? record = _lastSavedPressure;
+ BodyweightRecord? weight = _lastSavedWeight;
+ MedicineIntake? intake = _lastSavedIntake;
+
+ final noteFormValue = _noteForm.currentState?.save();
+ if (noteFormValue != null) {
+ note = Note(time: time, note: noteFormValue.$1, color: noteFormValue.$2?.value);
+ }
+ final recordFormValue = _bpForm.currentState?.save();
+ if (recordFormValue != null) {
+ final unit = context.read<Settings>().preferredPressureUnit;
+ record = BloodPressureRecord(
+ time: time,
+ sys: recordFormValue.sys == null ? null : unit.wrap(recordFormValue.sys!),
+ dia: recordFormValue.dia == null ? null : unit.wrap(recordFormValue.dia!),
+ pul: recordFormValue.pul,
+ );
+ }
+ final weightFormValue = _weightForm.currentState?.save();
+ if (weightFormValue != null) {
+ weight = BodyweightRecord(time: time, weight: weightFormValue);
+ }
+ final intakeFormValue = _intakeForm.currentState?.save();
+ if (intakeFormValue != null) {
+ intake = MedicineIntake(
+ time: time,
+ medicine: intakeFormValue.$1,
+ dosis: intakeFormValue.$2,
+ );
+ }
+
+ if (note == null
+ && record == null
+ && weight == null
+ && intake == null) {
+ return null;
+ }
+ return (
+ timestamp: time,
+ note: note,
+ record: record,
+ intake: intake,
+ weight: weight,
+ );
+ }
+
+ @override
+ bool isEmptyInputFocused() => false; // doesn't contain text inputs
+
+ @override
+ void fillForm(AddEntryFormValue? value) {
+ _lastSavedPressure = value?.record;
+ _lastSavedWeight = value?.weight;
+ _lastSavedIntake = value?.intake;
+ if (value == null) {
+ _timeForm.currentState?.fillForm(null);
+ _noteForm.currentState?.fillForm(null);
+ _bpForm.currentState?.fillForm(null);
+ _weightForm.currentState?.fillForm(null);
+ _intakeForm.currentState?.fillForm(null);
+ } else {
+ _timeForm.currentState?.fillForm(value.timestamp);
+ if (value.note != null) {
+ final c = value.note?.color == null ? null : Color(value.note!.color!);
+ _noteForm.currentState?.fillForm((value.note!.note, c));
+ }
+ if (value.record != null) {
+ _bpForm.currentState?.fillForm((
+ sys: value.record?.sys?.mmHg,
+ dia: value.record?.dia?.mmHg,
+ pul: value.record?.pul,
+ ));
+ }
+ if (value.weight != null) {
+ _weightForm.currentState?.fillForm(value.weight!.weight);
+ }
+ if (value.intake != null) {
+ _intakeForm.currentState?.fillForm((
+ value.intake!.medicine,
+ value.intake!.dosis,
+ ));
+ }
+ }
+ }
+
+ void _onExternalMeasurement(BloodPressureRecord record) => fillForm((
+ timestamp: record.time,
+ note: null,
+ record: record,
+ intake: null,
+ weight: null,
+ ));
+
+ @override
+ Widget build(BuildContext context) {
+ final settings = context.watch<Settings>();
+ return ListView(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ children: [
+ (() => switch (settings.bleInput) {
+ BluetoothInputMode.disabled => SizedBox.shrink(),
+ BluetoothInputMode.oldBluetoothInput => OldBluetoothInput(
+ onMeasurement: _onExternalMeasurement,
+ ),
+ BluetoothInputMode.newBluetoothInputOldLib => BluetoothInput(
+ manager: BluetoothManager.create(BluetoothBackend.flutterBluePlus),
+ onMeasurement: _onExternalMeasurement,
+ bluetoothCubit: widget.bluetoothCubit,
+ ),
+ BluetoothInputMode.newBluetoothInputCrossPlatform => BluetoothInput(
+ manager: BluetoothManager.create(BluetoothBackend.bluetoothLowEnergy),
+ onMeasurement: _onExternalMeasurement,
+ bluetoothCubit: widget.bluetoothCubit,
+ ),
+ })(),
+ if (settings.allowManualTimeInput)
+ DateTimeForm(
+ key: _timeForm,
+ initialValue: widget.initialValue?.timestamp,
+ ),
+ SizedBox(height: 10),
+ FormSwitcher(
+ key: Key('AddEntryFormSwitcher'), // ensures widgets are in tree
+ controller: _controller,
+ subForms: [
+ (Icon(Icons.monitor_heart_outlined), BloodPressureForm(
+ key: _bpForm,
+ initialValue: (
+ sys: widget.initialValue?.record?.sys?.mmHg,
+ dia: widget.initialValue?.record?.dia?.mmHg,
+ pul: widget.initialValue?.record?.pul,
+ ),
+ )),
+ if (widget.meds.isNotEmpty)
+ (Icon(Icons.medication_outlined), MedicineIntakeForm(
+ key: _intakeForm,
+ meds: widget.meds,
+ initialValue: widget.initialValue?.intake == null ? null : (
+ widget.initialValue!.intake!.medicine,
+ widget.initialValue!.intake!.dosis,
+ ),
+ )),
+ if (settings.weightInput)
+ (Icon(Icons.scale), WeightForm(
+ key: _weightForm,
+ initialValue: widget.initialValue?.weight?.weight,
+ ),),
+ ],
+ ),
+ NoteForm(
+ key: _noteForm,
+ initialValue: (){
+ if (widget.initialValue?.note?.note == null) return null;
+ final note = widget.initialValue!.note!;
+ final color = note.color == null ? null : Color(note.color!);
+ return (note.note, color);
+ }(),
+ ),
+ ]
+ );
+ }
+}
+
+/// Types of entries supported by [AddEntryForm].
+typedef AddEntryFormValue = ({
+ DateTime timestamp,
+ Note? note,
+ BloodPressureRecord? record,
+ MedicineIntake? intake,
+ BodyweightRecord? weight,
+});
+
+/// Compatibility extension for simpler API surface.
+extension AddEntryFormValueCompat on FullEntry {
+ /// Utility converter for the differences in API.
+ AddEntryFormValue get asAddEntry {
+ assert(intakes.length <= 1);
+ return (
+ timestamp: time,
+ note: (note != null && color == null) ? null : noteObj,
+ record: (sys == null && dia == null && pul == null) ? null : recordObj,
+ intake: intakes.firstOrNull,
+ weight: null,
+ );
+ }
+}
app/lib/features/input/forms/blood_pressure_form.dart
@@ -0,0 +1,168 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+
+/// Form to enter freeform text and select color.
+class BloodPressureForm extends FormBase<({int? sys, int? dia, int? pul})> {
+ /// Create form to enter freeform text and select color.
+ const BloodPressureForm({super.key,
+ super.initialValue,
+ });
+
+ @override
+ BloodPressureFormState createState() => BloodPressureFormState();
+}
+
+/// State of form to enter freeform text and select color.
+class BloodPressureFormState extends FormStateBase<({int? sys, int? dia, int? pul}), BloodPressureForm> {
+ final _formKey = GlobalKey<FormState>();
+
+ final _sysFocusNode = FocusNode();
+ final _diaFocusNode = FocusNode();
+ final _pulFocusNode = FocusNode();
+
+ late final TextEditingController _sysController;
+ late final TextEditingController _diaController;
+ late final TextEditingController _pulController;
+
+ @override
+ void initState() {
+ super.initState();
+ _sysController = TextEditingController(text: widget.initialValue?.sys?.toString() ?? '');
+ _diaController = TextEditingController(text: widget.initialValue?.dia?.toString() ?? '');
+ _pulController = TextEditingController(text: widget.initialValue?.pul?.toString() ?? '');
+ _sysFocusNode.requestFocus();
+ }
+
+ @override
+ void dispose() {
+ _sysFocusNode.dispose();
+ _diaFocusNode.dispose();
+ _pulFocusNode.dispose();
+ _sysController.dispose();
+ _diaController.dispose();
+ _pulController.dispose();
+ super.dispose();
+ }
+
+ @override
+ bool validate() {
+ if (_sysController.text.isEmpty
+ && _diaController.text.isEmpty
+ && _pulController.text.isEmpty) {
+ return true;
+ }
+ return _formKey.currentState?.validate() ?? false;
+ }
+
+ @override
+ ({int? sys, int? dia, int? pul})? save() {
+ if (!validate()
+ || (int.tryParse(_sysController.text) == null
+ && int.tryParse(_diaController.text) == null
+ && int.tryParse(_pulController.text) == null)) {
+ return null;
+ }
+ return (
+ sys: int.tryParse(_sysController.text),
+ dia: int.tryParse(_diaController.text),
+ pul: int.tryParse(_pulController.text),
+ );
+ }
+
+ @override
+ bool isEmptyInputFocused() => (_diaFocusNode.hasFocus && _diaController.text.isEmpty)
+ || (_pulFocusNode.hasFocus && _pulController.text.isEmpty);
+
+ @override
+ void fillForm(({int? dia, int? pul, int? sys})? value) => setState(() {
+ if (value == null) {
+ _sysController.text = '';
+ _diaController.text = '';
+ _pulController.text = '';
+ } else {
+ if (value.dia != null) _diaController.text = value.dia.toString();
+ if (value.pul != null) _pulController.text = value.pul.toString();
+ if (value.sys != null) _sysController.text = value.sys.toString();
+ }
+ });
+
+ Widget _buildValueInput({
+ String? labelText,
+ FocusNode? focusNode,
+ TextEditingController? controller,
+ String? Function(String?)? validator,
+ }) => Expanded(
+ child: TextFormField(
+ focusNode: focusNode,
+ controller: controller,
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ onChanged: (String value) {
+ if (value.isNotEmpty
+ && (int.tryParse(value) ?? -1) > 40) {
+ FocusScope.of(context).nextFocus();
+ }
+ },
+ validator: (String? value) {
+ final settings = context.read<Settings>();
+ if (!settings.allowMissingValues
+ && (value == null
+ || value.isEmpty
+ || int.tryParse(value) == null)) {
+ return AppLocalizations.of(context)!.errNaN;
+ } else if (settings.validateInputs
+ && (int.tryParse(value ?? '') ?? -1) <= 30) {
+ return AppLocalizations.of(context)!.errLt30;
+ } else if (settings.validateInputs
+ && (int.tryParse(value ?? '') ?? 0) >= 400) {
+ // https://pubmed.ncbi.nlm.nih.gov/7741618/
+ return AppLocalizations.of(context)!.errUnrealistic;
+ }
+ return validator?.call(value);
+ },
+ decoration: InputDecoration(
+ labelText: labelText,
+ ),
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ );
+
+ @override
+ Widget build(BuildContext context) => Form(
+ key: _formKey,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildValueInput(
+ focusNode: _sysFocusNode,
+ controller: _sysController,
+ labelText: AppLocalizations.of(context)!.sysLong,
+ ),
+ const SizedBox(width: 8,),
+ _buildValueInput(
+ labelText: AppLocalizations.of(context)!.diaLong,
+ controller: _diaController,
+ focusNode: _diaFocusNode,
+ validator: (value) {
+ if (context.read<Settings>().validateInputs
+ && (int.tryParse(value ?? '') ?? 0)
+ >= (int.tryParse(_sysController.text) ?? 1)) {
+ return AppLocalizations.of(context)?.errDiaGtSys;
+ }
+ return null;
+ },
+ ),
+ const SizedBox(width: 8,),
+ _buildValueInput(
+ controller: _pulController,
+ focusNode: _pulFocusNode,
+ labelText: AppLocalizations.of(context)!.pulLong,
+ ),
+ ],
+ ),
+ );
+}
app/lib/features/input/forms/date_time_form.dart
@@ -1,88 +1,109 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
/// Input to allow date and time input.
-class DateTimeForm extends StatefulWidget {
+class DateTimeForm extends FormBase<DateTime> {
/// Create input to allow date and time input.
const DateTimeForm({super.key,
- required this.initialTime,
- required this.validate,
- required this.onTimeSelected,
+ super.initialValue,
});
- /// Initial time to display
- final DateTime initialTime;
+ @override
+ FormStateBase<DateTime, DateTimeForm> createState() => DateTimeFormState();
+}
- /// Whether to validate whether the time is after now.
- final bool validate;
+/// State of a [DateTimeForm].
+class DateTimeFormState extends FormStateBase<DateTime, DateTimeForm> {
+ late DateTime _time;
- /// Call after a new time is successfully selected.
- final void Function(DateTime time) onTimeSelected;
+ String? _error;
@override
- State<DateTimeForm> createState() => _DateTimeFormState();
-}
+ void initState() {
+ super.initState();
+ _time = widget.initialValue ?? DateTime.now();
+ }
+
+ @override
+ DateTime? save() => validate() ? _time : null;
+
+ @override
+ bool isEmptyInputFocused() => false;
+
+ @override
+ bool validate() {
+ if (context.read<Settings>().validateInputs && _time.isAfter(DateTime.now())) {
+ setState(() {
+ _error = AppLocalizations.of(context)!.errTimeAfterNow;
+ });
+ return false;
+ } else if (_error != null) {
+ setState(() {
+ _error = null;
+ });
+ }
+ return true;
+ }
+
+ @override
+ void fillForm(DateTime? value) => setState(() {
+ _time = value ?? DateTime.now();
+ });
-class _DateTimeFormState extends State<DateTimeForm> {
Future<void> _openDatePicker() async {
final now = DateTime.now();
final date = await showDatePicker(
context: context,
- initialDate: widget.initialTime,
+ initialDate: _time,
firstDate: DateTime.fromMillisecondsSinceEpoch(1),
- lastDate: widget.initialTime.isAfter(now) ? widget.initialTime : now,
+ lastDate: _time.isAfter(now) ? _time : now,
);
if (date == null) return;
- _validateAndInvoke(date.copyWith(
- hour: widget.initialTime.hour,
- minute: widget.initialTime.minute,
+ setState(() => _time = date.copyWith(
+ hour: _time.hour,
+ minute: _time.minute,
));
+
}
Future<void> _openTimePicker() async {
- final time = await showTimePicker(
+ final timeOfDay = await showTimePicker(
context: context,
- initialTime: TimeOfDay.fromDateTime(widget.initialTime),
+ initialTime: TimeOfDay.fromDateTime(_time),
);
- if (time == null) return;
- _validateAndInvoke(widget.initialTime.copyWith(
- hour: time.hour,
- minute: time.minute,
+ if (timeOfDay == null) return;
+ setState(() => _time = _time.copyWith(
+ hour: timeOfDay.hour,
+ minute: timeOfDay.minute,
));
}
- void _validateAndInvoke(DateTime time) {
- if (widget.validate && time.isAfter(DateTime.now())) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text(AppLocalizations.of(context)!.errTimeAfterNow),
- ));
- return;
- }
- widget.onTimeSelected(time);
- }
-
- Widget _buildInput(String content, void Function() onTap, String label) => Expanded(
+ Widget _buildInput(String content, void Function() onTap, String label, [String? error]) => Expanded(
child: InputDecorator(
+ decoration: InputDecoration(
+ labelText: label,
+ error: error == null ? null : Text(error),
+ ),
child: GestureDetector(
onTap: onTap,
child: Text(content, style: Theme.of(context).textTheme.bodyLarge)
),
- decoration: InputDecoration(
- labelText: label,
- ),
),
);
@override
Widget build(BuildContext context) {
- final date = DateFormat('yyyy-MM-dd').format(widget.initialTime);
- final time = DateFormat('HH:mm').format(widget.initialTime);
+ final date = DateFormat('yyyy-MM-dd').format(_time);
+ final timeOfDay = DateFormat('HH:mm').format(_time);
return Row(
children: [
_buildInput(date, _openDatePicker, AppLocalizations.of(context)!.date),
SizedBox(width: 8,),
- _buildInput(time, _openTimePicker, AppLocalizations.of(context)!.time),
+ _buildInput(timeOfDay, _openTimePicker, AppLocalizations.of(context)!.time, _error),
],
);
}
app/lib/features/input/forms/form_base.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+/// Base for a generic form with return value [T].
+abstract class FormBase<T> extends StatefulWidget {
+ /// Create a form with generic return value.
+ const FormBase({super.key, this.initialValue});
+
+ /// Initial value to prefill the form with.
+ final T? initialValue;
+
+ @override
+ FormStateBase createState();
+}
+
+/// State of a form allowing validation and result gathering using [GlobalKey].
+///
+/// ### Sample usage
+/// ```dart
+/// class SomeWidgetState extends State<SomeWidget> {
+/// final key = GlobalKey<FormStateBase>();
+/// ...
+/// FormBase(key: key),
+/// ...
+/// TextButton(
+/// child: Text('save'),
+/// onPressed: () => if (_timeFormState.currentState?.validate() ?? false) {
+/// Navigator.pop(context, _timeFormState.currentState!.save());
+/// },
+/// )
+/// ```
+abstract class FormStateBase<T, G extends FormBase> extends State<G> {
+ /// Validates all form fields and shows errors on failing form fields.
+ ///
+ /// Returns whether the all fields validated without error.
+ bool validate();
+
+ /// Parses and returns the forms current value, if [validate] passes.
+ T? save();
+
+ /// Whether an empty input field is focused.
+ ///
+ /// Used to automatically focus the last input field on back key.
+ bool isEmptyInputFocused();
+
+ /// Set the input fields with the [value].
+ ///
+ /// If [value} is null clear the form. If value contains attributes that
+ /// correspond to different fields, only the non null attributes change field
+ /// contents.
+ void fillForm(T? value);
+}
app/lib/features/input/forms/form_switcher.dart
@@ -0,0 +1,100 @@
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+import 'package:inline_tab_view/inline_tab_view.dart';
+
+/// A resizing view that associates a tab-bar and child widgets.
+class FormSwitcher extends StatefulWidget {
+ /// Create a resizing view that associates a tab-bar and child widgets-
+ const FormSwitcher({super.key,
+ required this.subForms,
+ this.controller,
+ });
+
+ /// List of (tab title, tab content) pairs.
+ final List<(Widget, Widget)> subForms;
+
+ /// Controller to use to control the switcher from code.
+ final FormSwitcherController? controller;
+
+ @override
+ State<FormSwitcher> createState() => _FormSwitcherState();
+}
+
+class _FormSwitcherState extends State<FormSwitcher>
+ with TickerProviderStateMixin {
+ late final TabController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = TabController(length: widget.subForms.length, vsync: this);
+ widget.controller?._initialize(controller);
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ assert(widget.subForms.isNotEmpty);
+ if (widget.subForms.length == 1) {
+ return widget.subForms[0].$2;
+ }
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TabBar.secondary(
+ controller: controller,
+ tabs: [
+ for (final f in widget.subForms)
+ Padding(
+ padding: EdgeInsets.all(8.0),
+ child: f.$1,
+ ),
+ ],
+ ),
+ InlineTabView(
+ controller: controller,
+ children: [
+ for (final f in widget.subForms)
+ Padding(
+ padding: EdgeInsets.only(top: 8.0),
+ child: f.$2,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+/// Allows controlling a [FormSwitcher] from code.
+class FormSwitcherController {
+ final Queue<Function> _pendingActions = Queue();
+
+ TabController? _controller;
+
+ /// Add a reference to a TabController to control.
+ ///
+ /// This does not mean this object is responsible for destroying it.
+ void _initialize(TabController controller) {
+ assert(_controller == null, 'FormSwitcherController was initialized twice');
+ _controller = controller;
+ while (_pendingActions.isNotEmpty) {
+ _pendingActions.removeFirst().call();
+ }
+ }
+
+ /// Animates to viewing the page at the specified index as soon as possible.
+ void animateTo(int index) {
+ if (_controller == null) {
+ _pendingActions.add(() => animateTo(index));
+ } else {
+ _controller!.animateTo(index);
+ }
+ }
+}
app/lib/features/input/forms/medicine_intake_form.dart
@@ -0,0 +1,114 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+/// Form to enter medicine intakes.
+class MedicineIntakeForm extends FormBase<(Medicine, Weight)> {
+ /// Create form to enter medicine intakes.
+ MedicineIntakeForm({super.key,
+ super.initialValue,
+ required this.meds,
+ }) : assert(meds.isNotEmpty);
+
+ /// All selectable medicines.
+ final List<Medicine> meds;
+
+ @override
+ FormStateBase<(Medicine, Weight), MedicineIntakeForm> createState() =>
+ MedicineIntakeFormState();
+}
+
+/// State of form to enter medicine intakes.
+class MedicineIntakeFormState extends FormStateBase<(Medicine, Weight), MedicineIntakeForm> {
+ final _controller = TextEditingController();
+
+ Medicine? _leadingMed;
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller.text = _leadingMed?.dosis?.mg.toString() ?? '';
+
+ if (widget.initialValue != null) {
+ _leadingMed = widget.initialValue!.$1;
+ _controller.text = widget.initialValue!.$2.mg.toString();
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ bool validate() {
+ if (_leadingMed != null && double.tryParse(_controller.text) == null) {
+ setState(() => _error = AppLocalizations.of(context)!.errNaN);
+ return false;
+ }
+ setState(() => _error = null);
+ return true;
+ }
+
+ @override
+ (Medicine, Weight)? save() {
+ if (_leadingMed == null || !validate()) return null;
+ return (_leadingMed!, Weight.mg(double.parse(_controller.text)));
+ }
+
+ @override
+ bool isEmptyInputFocused() => false;
+
+ @override
+ void fillForm((Medicine, Weight)? value) => setState(() {
+ if (value == null) {
+ _leadingMed = null;
+ _controller.text = '';
+ } else {
+ _leadingMed = value.$1;
+ _controller.text = value.$2.mg.toString();
+ }
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (_leadingMed != null) {
+ return TextField(
+ decoration: InputDecoration(
+ helperText: _leadingMed!.designation,
+ labelText: AppLocalizations.of(context)!.dosis,
+ prefixIcon: Icon(Icons.medication,
+ color: _leadingMed!.color == null ? null : Color(_leadingMed!.color!)),
+ suffixIcon: IconButton(
+ onPressed: () => setState(() => _leadingMed = null),
+ icon: Icon(Icons.close),
+ ),
+ errorText: _error,
+ ),
+ controller: _controller,
+ inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ );
+ }
+ return Column(
+ children: [
+ for (final m in widget.meds)
+ ListTile(
+ leading: Icon(Icons.medication, color: m.color == null ? null : Color(m.color!)),
+ title: Text(m.designation),
+ subtitle: (widget.meds.length == 1)
+ ? Text(AppLocalizations.of(context)!.tapToSelect)
+ : null,
+ onTap: () => setState(() {
+ _leadingMed = m;
+ _controller.text = _leadingMed?.dosis?.mg.toString() ?? '';
+ }),
+ ),
+ ],
+ );
+ }
+}
app/lib/features/input/forms/note_form.dart
@@ -0,0 +1,92 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+/// Form to enter freeform text and select color.
+class NoteForm extends FormBase<(String?, Color?)> {
+ /// Create form to enter freeform text and select color.
+ const NoteForm({super.key,
+ super.initialValue,
+ });
+
+ @override
+ NoteFormState createState() => NoteFormState();
+}
+
+/// State of form to enter freeform text and select color.
+class NoteFormState extends FormStateBase<(String?, Color?), NoteForm> {
+ late final TextEditingController _controller;
+
+ final FocusNode _focusNode = FocusNode();
+
+ Color? _color;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = TextEditingController(text: widget.initialValue?.$1);
+ _color = widget.initialValue?.$2;
+ }
+
+ @override
+ void dispose() {
+ _focusNode.dispose();
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ bool validate() => true;
+
+ @override
+ (String?, Color?)? save() {
+ final String? text = _controller.text.isEmpty ? null : _controller.text;
+ if (text == null && _color == null) return null;
+ return (text, _color);
+ }
+
+ @override
+ bool isEmptyInputFocused() => _focusNode.hasFocus && _controller.text.isEmpty;
+
+ @override
+ void fillForm((String?, Color?)? value) => setState(() {
+ if (value == null) {
+ _controller.text = '';
+ _color = null;
+ } else {
+ if (value.$1 != null) _controller.text = value.$1!;
+ if (value.$2 != null) _color = value.$2!;
+ }
+ });
+
+ @override
+ Widget build(BuildContext context) => Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: TextFormField(
+ focusNode: _focusNode,
+ controller: _controller,
+ decoration: InputDecoration(
+ labelText: AppLocalizations.of(context)!.addNote,
+ ),
+ minLines: 1,
+ maxLines: 4,
+ ),
+ ),
+ InputDecorator(
+ decoration: InputDecoration(
+ contentPadding: EdgeInsets.zero,
+ ),
+ child: ColorSelectionListTile(
+ title: Text(AppLocalizations.of(context)!.color, style: Theme.of(context).textTheme.bodyLarge,),
+ onMainColorChanged: (Color value) => setState(() {
+ _color = (value == Colors.transparent) ? null : value;
+ }),
+ initialColor: _color ?? Colors.transparent,
+ ),
+ ),
+ ],
+ );
+}
app/lib/features/input/forms/weight_form.dart
@@ -0,0 +1,81 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
+
+/// A form to enter [Weight] in the preferred unit.
+class WeightForm extends FormBase<Weight> {
+ /// Create a form to enter [Weight] in the preferred unit.
+ const WeightForm({super.key, super.initialValue});
+
+ @override
+ FormStateBase<Weight, WeightForm> createState() => WeightFormState();
+}
+
+/// State of a form to enter [Weight] in the preferred unit.
+class WeightFormState extends FormStateBase<Weight, WeightForm> {
+ final TextEditingController _controller = TextEditingController();
+
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.initialValue != null) {
+ final w = context.read<Settings>().weightUnit.extract(widget.initialValue!);
+ _controller.text = w.toString();
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ bool validate() {
+ if (_controller.text.isNotEmpty && double.tryParse(_controller.text) == null) {
+ setState(() => _error = AppLocalizations.of(context)!.errNaN);
+ return false;
+ }
+ setState(() => _error = null);
+ return true;
+ }
+
+ @override
+ Weight? save() {
+ if((validate(), double.tryParse(_controller.text)) case (true, final double x)) {
+ return context.read<Settings>().weightUnit.store(x);
+ }
+ return null;
+ }
+
+ @override
+ bool isEmptyInputFocused() => false;
+
+ @override
+ void fillForm(Weight? value) {
+ if (value == null) {
+ _controller.text = '';
+ } else {
+ final w = context.read<Settings>().weightUnit.extract(widget.initialValue!);
+ _controller.text = w.toString();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) => TextField(
+ decoration: InputDecoration(
+ labelText: AppLocalizations.of(context)!.weight,
+ suffix: Text(context.select((Settings s) => s.weightUnit).name),
+ errorText: _error,
+ ),
+ controller: _controller,
+ inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ );
+}
app/lib/features/input/add_bodyweight_dialoge.dart
@@ -1,47 +0,0 @@
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart';
-
-/// A simple dialoge to enter one weight value in kg.
-///
-/// Returns a [Weight] on submission.
-class AddBodyweightDialoge extends StatelessWidget {
- /// Create a simple dialoge to enter one weight value in kg.
- const AddBodyweightDialoge({super.key});
-
- @override
- Widget build(BuildContext context) => Dialog(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 2.0),
- child: TextFormField(
- autofocus: true,
- decoration: InputDecoration(
- labelText: AppLocalizations.of(context)!.weight,
- suffix: Text(context.select((Settings s) => s.weightUnit).name),
- ),
- inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
- keyboardType: const TextInputType.numberWithOptions(decimal: true),
- autovalidateMode: AutovalidateMode.onUnfocus,
- validator: (value) {
- if (value == null
- || value.isEmpty
- || double.tryParse(value) == null
- ) {
- return AppLocalizations.of(context)!.errNaN;
- }
- return null;
- },
- onFieldSubmitted: (String text) {
- final value = double.tryParse(text);
- if (value != null) {
- final weight = context.read<Settings>().weightUnit.store(value);
- Navigator.of(context).pop(weight);
- }
- },
- ),
- ),
- );
-}
app/lib/features/input/add_entry_dialogue.dart
@@ -0,0 +1,78 @@
+import 'dart:async';
+
+import 'package:blood_pressure_app/components/fullscreen_dialoge.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+/// Input mask for entering measurements.
+class AddEntryDialogue extends StatefulWidget {
+ /// Create a input mask for entering measurements.
+ ///
+ /// This is usually created through the [showAddEntryDialogue] function.
+ const AddEntryDialogue({super.key,
+ this.availableMeds,
+ this.initialRecord,
+ });
+
+ /// Values that are prefilled.
+ ///
+ /// When this is null the timestamp is [DateTime.now] and the other fields
+ /// will be empty.
+ final AddEntryFormValue? initialRecord;
+
+ /// All medicines selectable.
+ ///
+ /// Hides med input when this is empty or null.
+ final List<Medicine>? availableMeds;
+
+ @override
+ State<AddEntryDialogue> createState() => _AddEntryDialogueState();
+}
+
+class _AddEntryDialogueState extends State<AddEntryDialogue> {
+ final formKey = GlobalKey<AddEntryFormState>();
+
+ void _onSavePressed() {
+ if (formKey.currentState?.validate() ?? false) {
+ final AddEntryFormValue? result = formKey.currentState?.save();
+ Navigator.pop(context, result);
+ } else {
+ // Errors are displayed below their specific widgets
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) => FullscreenDialoge(
+ actionButtonText: AppLocalizations.of(context)!.btnSave,
+ onActionButtonPressed: _onSavePressed,
+ bottomAppBar: context.select((Settings s) => s.bottomAppBars),
+ body: AddEntryForm(
+ key: formKey,
+ initialValue: widget.initialRecord,
+ meds: widget.availableMeds ?? [],
+ ),
+ );
+}
+
+/// Shows a dialogue to input a blood pressure measurement or a medication.
+Future<AddEntryFormValue?> showAddEntryDialogue(
+ BuildContext context,
+ MedicineRepository medRepo,
+ [AddEntryFormValue? initialRecord,
+]) async {
+ final meds = await medRepo.getAll();
+ if (context.mounted) {
+ return showDialog<AddEntryFormValue>(
+ context: context, builder: (context) =>
+ AddEntryDialogue(
+ initialRecord: initialRecord,
+ availableMeds: meds,
+ ),
+ );
+ }
+ return null;
+}
app/lib/features/input/add_measurement_dialoge.dart
@@ -1,461 +0,0 @@
-import 'dart:async';
-import 'dart:io';
-
-import 'package:blood_pressure_app/components/fullscreen_dialoge.dart';
-import 'package:blood_pressure_app/config.dart';
-import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
-import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
-import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
-import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
-import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
-import 'package:blood_pressure_app/model/storage/storage.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart';
-import 'package:provider/provider.dart';
-
-/// Input mask for entering measurements.
-class AddEntryDialoge extends StatefulWidget {
- /// Create a input mask for entering measurements.
- ///
- /// This is usually created through the [showAddEntryDialoge] function.
- const AddEntryDialoge({super.key,
- required this.availableMeds,
- this.initialRecord,
- });
-
- /// Values that are prefilled.
- ///
- /// When this is null the timestamp is [DateTime.now] and the other fields
- /// will be empty.
- ///
- /// When an initial record is set medicine input is not possible because it is
- /// saved separately.
- final FullEntry? initialRecord;
-
- /// All medicines selectable.
- ///
- /// Hides med input when this is empty.
- final List<Medicine> availableMeds;
-
- @override
- State<AddEntryDialoge> createState() => _AddEntryDialogeState();
-}
-
-class _AddEntryDialogeState extends State<AddEntryDialoge> {
- final recordFormKey = GlobalKey<FormState>();
- final medicationFormKey = GlobalKey<FormState>();
-
- final sysFocusNode = FocusNode();
- final diaFocusNode = FocusNode();
- final pulFocusNode = FocusNode();
- final noteFocusNode = FocusNode();
- final dosisFocusNote = FocusNode();
-
- late final TextEditingController sysController;
- late final TextEditingController diaController;
- late final TextEditingController pulController;
- late final TextEditingController noteController;
-
- /// Currently selected time.
- late DateTime time;
-
- /// Current selected note color.
- Color? color;
-
- /// Last [FormState.save]d systolic value.
- int? systolic;
-
- /// Last [FormState.save]d diastolic value.
- int? diastolic;
-
- /// Last [FormState.save]d pulse value.
- int? pulse;
-
- /// Medicine to save.
- Medicine? selectedMed;
-
- /// Whether to show the medication dosis input
- bool _showMedicineDosisInput = false;
-
- /// Entered dosis of medication.
- ///
- /// Prefilled with default dosis of selected medicine.
- double? medicineDosis;
-
- @override
- void initState() {
- super.initState();
- sysController = TextEditingController();
- diaController = TextEditingController();
- pulController = TextEditingController();
- noteController = TextEditingController();
- _loadFields(widget.initialRecord);
-
- sysFocusNode.requestFocus();
- ServicesBinding.instance.keyboard.addHandler(_onKey);
- }
-
-
- @override
- void dispose() {
- sysController.dispose();
- diaController.dispose();
- pulController.dispose();
- noteController.dispose();
-
- sysFocusNode.dispose();
- diaFocusNode.dispose();
- pulFocusNode.dispose();
- noteFocusNode.dispose();
- ServicesBinding.instance.keyboard.removeHandler(_onKey);
- super.dispose();
- }
-
- /// Sets fields to values in a [record].
- void _loadFields(FullEntry? entry) {
- final settings = context.read<Settings>();
- time = entry?.time ?? DateTime.now();
- final int? colorValue = entry?.color;
- final sysValue = switch(settings.preferredPressureUnit) {
- PressureUnit.mmHg => entry?.sys?.mmHg,
- PressureUnit.kPa => entry?.sys?.kPa.round(),
- };
- final diaValue = switch(settings.preferredPressureUnit) {
- PressureUnit.mmHg => entry?.dia?.mmHg,
- PressureUnit.kPa => entry?.dia?.kPa.round(),
- };
- if (colorValue != null) color = Color(colorValue);
- if (entry?.sys != null) sysController.text = sysValue.toString();
- if (entry?.dia != null) diaController.text = diaValue.toString();
- if (entry?.pul != null) pulController.text = entry!.pul!.toString();
- if (entry?.note != null) noteController.text = entry!.note!;
- }
-
- bool _onKey(KeyEvent event) {
- if (event is! KeyDownEvent) return false;
- final isBackspace = event.logicalKey.keyId == 0x00100000008;
- if (!isBackspace) return false;
- recordFormKey.currentState?.save();
- if (diaFocusNode.hasFocus && diastolic == null
- || pulFocusNode.hasFocus && pulse == null
- || noteFocusNode.hasFocus && noteController.text.isEmpty
- ) {
- FocusScope.of(context).previousFocus();
- }
- return false;
- }
-
- /// Build a input for values in the measurement form (sys, dia, pul).
- Widget _buildValueInput(AppLocalizations localizations, Settings settings, {
- String? labelText,
- void Function(String?)? onSaved,
- FocusNode? focusNode,
- TextEditingController? controller,
- String? Function(String?)? validator,
- }) => Expanded(
- child: TextFormField(
- decoration: InputDecoration(
- labelText: labelText,
- ),
- keyboardType: TextInputType.number,
- focusNode: focusNode,
- onSaved: onSaved,
- controller: controller,
- style: Theme.of(context).textTheme.bodyLarge,
- inputFormatters: [FilteringTextInputFormatter.digitsOnly],
- onChanged: (String value) {
- if (value.isNotEmpty
- && (int.tryParse(value) ?? -1) > 40) {
- FocusScope.of(context).nextFocus();
- }
- },
- validator: (String? value) {
- if (!settings.allowMissingValues
- && (value == null
- || value.isEmpty
- || int.tryParse(value) == null)) {
- return localizations.errNaN;
- } else if (settings.validateInputs
- && (int.tryParse(value ?? '') ?? -1) <= 30) {
- return localizations.errLt30;
- } else if (settings.validateInputs
- && (int.tryParse(value ?? '') ?? 0) >= 400) {
- // https://pubmed.ncbi.nlm.nih.gov/7741618/
- return localizations.errUnrealistic;
- }
- return validator?.call(value);
- },
- ),
- );
-
- void _onExternalMeasurement(BloodPressureRecord record) => setState(() {
- final note = Note(time: record.time, note: noteController.text, color: color?.value);
- _loadFields((record, note, []));
- });
-
- @override
- Widget build(BuildContext context) {
- final localizations = AppLocalizations.of(context)!;
- final settings = context.watch<Settings>();
- return FullscreenDialoge(
- onActionButtonPressed: () {
- BloodPressureRecord? record;
- Note? note;
- final List<MedicineIntake> intakes = [];
-
- final bool shouldHaveRecord = (sysController.text.isNotEmpty
- || diaController.text.isNotEmpty
- || pulController.text.isNotEmpty);
-
- if (shouldHaveRecord && (recordFormKey.currentState?.validate() ?? false)) {
- recordFormKey.currentState?.save();
- if (systolic != null || diastolic != null || pulse != null) {
- final pressureUnit = settings.preferredPressureUnit;
- record = BloodPressureRecord(
- time: time,
- sys: systolic == null ? null : pressureUnit.wrap(systolic!),
- dia: diastolic == null ? null : pressureUnit.wrap(diastolic!),
- pul: pulse,
- );
- }
- }
-
-
- if (noteController.text.isNotEmpty || color != null) {
- note = Note(
- time: time,
- note: noteController.text.isEmpty ? null : noteController.text,
- color: color?.value,
- );
- }
- if (_showMedicineDosisInput
- && (medicationFormKey.currentState?.validate() ?? false)) {
- medicationFormKey.currentState?.save();
- if (medicineDosis != null
- && selectedMed != null) {
- intakes.add(MedicineIntake(
- time: time,
- medicine: selectedMed!,
- dosis: Weight.mg(medicineDosis!),
- ));
- }
- }
-
- if (record != null || intakes.isNotEmpty || note != null) {
- if (record == null && shouldHaveRecord) return; // Errors are shown
- if (intakes.isEmpty && _showMedicineDosisInput) return; // Errors are shown
- record ??= BloodPressureRecord(time: time);
- note ??= Note(time: time);
- Navigator.pop(context, (record, note, intakes));
- }
- },
- actionButtonText: localizations.btnSave,
- bottomAppBar: settings.bottomAppBars,
- body: SizeChangedLayoutNotifier(
- child: ListView(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- children: [
- if (!isTestingEnvironment) // TODO: test feature (#494)
- (() => switch (settings.bleInput) {
- BluetoothInputMode.disabled => SizedBox.shrink(),
- BluetoothInputMode.oldBluetoothInput => OldBluetoothInput(
- onMeasurement: _onExternalMeasurement,
- ),
- BluetoothInputMode.newBluetoothInputOldLib => BluetoothInput(
- manager: BluetoothManager.create(BluetoothBackend.flutterBluePlus),
- onMeasurement: _onExternalMeasurement,
- ),
- BluetoothInputMode.newBluetoothInputCrossPlatform => BluetoothInput(
- manager: BluetoothManager.create( BluetoothBackend.bluetoothLowEnergy),
- onMeasurement: _onExternalMeasurement,
- ),
- })(),
- if (settings.allowManualTimeInput)
- DateTimeForm(
- validate: settings.validateInputs,
- initialTime: time,
- onTimeSelected: (newTime) => setState(() {
- time = newTime;
- }),
- ),
- Form(
- key: recordFormKey,
- child: Column(
- children: [
- const SizedBox(height: 16,),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- _buildValueInput(localizations, settings,
- focusNode: sysFocusNode,
- labelText: localizations.sysLong,
- controller: sysController,
- onSaved: (value) =>
- setState(() => systolic = int.tryParse(value ?? '')),
- ),
- const SizedBox(width: 8,),
- _buildValueInput(localizations, settings,
- labelText: localizations.diaLong,
- controller: diaController,
- onSaved: (value) =>
- setState(() => diastolic = int.tryParse(value ?? '')),
- focusNode: diaFocusNode,
- validator: (value) {
- if (settings.validateInputs
- && (int.tryParse(value ?? '') ?? 0)
- >= (int.tryParse(sysController.text) ?? 1)
- ) {
- return AppLocalizations.of(context)?.errDiaGtSys;
- }
- return null;
- },
- ),
- const SizedBox(width: 8,),
- _buildValueInput(localizations, settings,
- labelText: localizations.pulLong,
- controller: pulController,
- focusNode: pulFocusNode,
- onSaved: (value) =>
- setState(() => pulse = int.tryParse(value ?? '')),
- ),
- ],
- ),
- ],
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 16),
- child: TextFormField(
- controller: noteController,
- focusNode: noteFocusNode,
- decoration: InputDecoration(
- labelText: localizations.addNote,
- ),
- minLines: 1,
- maxLines: 4,
- ),
- ),
- InputDecorator(
- decoration: const InputDecoration(
- contentPadding: EdgeInsets.zero,
- ),
- child: ColorSelectionListTile(
- title: Text(localizations.color, style: Theme.of(context).textTheme.bodyLarge,),
- onMainColorChanged: (Color value) => setState(() {
- color = (value == Colors.transparent) ? null : value;
- }),
- initialColor: color ?? Colors.transparent,
- ),
- ),
- if (widget.initialRecord == null && widget.availableMeds.isNotEmpty)
- 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 med in widget.availableMeds)
- DropdownMenuItem(
- value: med,
- child: Text(med.designation, style: Theme.of(context).textTheme.bodyLarge,),
- ),
- DropdownMenuItem(
- child: Text(localizations.noMedication, style: Theme.of(context).textTheme.bodyLarge,),
- ),
- ],
- onChanged: (v) {
- setState(() {
- if (v != null) {
- _showMedicineDosisInput = true;
- selectedMed = v;
- medicineDosis = v.dosis?.mg;
- dosisFocusNote.requestFocus();
- } else {
- _showMedicineDosisInput = false;
- selectedMed = null;
- }
- });
- },
- ),
- ),
- if (_showMedicineDosisInput)
- const SizedBox(width: 14,),
- if (_showMedicineDosisInput)
- Expanded(
- child: TextFormField(
- initialValue: medicineDosis?.toString(),
- decoration: InputDecoration(
- labelText: localizations.dosis,
- ),
- style: Theme.of(context).textTheme.bodyLarge,
- focusNode: dosisFocusNote,
- 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;
- },
- ),
- ),
- ],
- ),
- ),
- ),
-
- if (settings.weightInput)
- ListTile(
- title: Text(localizations.enterWeight),
- leading: const Icon(Icons.scale),
- trailing: const Icon(Icons.arrow_forward_ios),
- onTap: () async {
- final repo = context.read<BodyweightRepository>();
- final weight = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge());
- if (weight != null) {
- await repo.add(BodyweightRecord(time: time, weight: weight));
- if (context.mounted) Navigator.pop(context);
- }
- },
- ),
- ],
- ),
- ),
- );
- }
-}
-
-/// Shows a dialoge to input a blood pressure measurement or a medication.
-Future<FullEntry?> showAddEntryDialoge(
- BuildContext context,
- MedicineRepository medRepo,
- [FullEntry? initialRecord,
-]) async {
- final meds = await medRepo.getAll();
- return showDialog<FullEntry>(
- context: context, builder: (context) => AddEntryDialoge(
- initialRecord: initialRecord,
- availableMeds: meds,
- ),
- );
-}
app/lib/features/measurement_list/compact_measurement_list.dart
@@ -1,6 +1,7 @@
import 'package:blood_pressure_app/components/nullable_text.dart';
import 'package:blood_pressure_app/components/pressure_text.dart';
import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -70,7 +71,7 @@ class _CompactMeasurementListState extends State<CompactMeasurementList> {
key: Key(widget.data[index].time.toIso8601String()),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) { // edit
- await context.createEntry(widget.data[index]);
+ await context.createEntry(widget.data[index].asAddEntry);
return false;
} else { // delete
await context.deleteEntry(widget.data[index]);
app/lib/features/measurement_list/measurement_list.dart
@@ -1,4 +1,5 @@
import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
import 'package:blood_pressure_app/features/measurement_list/measurement_list_entry.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
@@ -70,7 +71,7 @@ class MeasurementList extends StatelessWidget {
itemCount: entries.length,
itemBuilder: (context, idx) => MeasurementListRow(
data: entries[idx],
- onRequestEdit: () => context.createEntry(entries[idx]),
+ onRequestEdit: () => context.createEntry(entries[idx].asAddEntry),
),
),
),
app/lib/features/measurement_list/weight_list.dart
@@ -1,4 +1,5 @@
import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
+import 'package:blood_pressure_app/data_util/entry_context.dart';
import 'package:blood_pressure_app/data_util/repository_builder.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:blood_pressure_app/model/weight_unit.dart';
@@ -28,14 +29,29 @@ class WeightList extends StatelessWidget {
itemBuilder: (context, idx) => ListTile(
title: Text(_buildWeightText(weightUnit, records[idx].weight)),
subtitle: Text(format.format(records[idx].time)),
- trailing: IconButton(
- icon: const Icon(Icons.delete),
- onPressed: () async {
- final repo = context.read<BodyweightRepository>();
- if ((!context.read<Settings>().confirmDeletion) || await showConfirmDeletionDialoge(context)) {
- await repo.remove(records[idx]);
- }
- }
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ icon: Icon(Icons.edit),
+ onPressed: () => context.createEntry((
+ timestamp: records[idx].time,
+ note: null,
+ record: null,
+ intake: null,
+ weight: records[idx],
+ )),
+ ),
+ IconButton(
+ icon: Icon(Icons.delete),
+ onPressed: () async {
+ final repo = context.read<BodyweightRepository>();
+ if ((!context.read<Settings>().confirmDeletion) || await showConfirmDeletionDialoge(context)) {
+ await repo.remove(records[idx]);
+ }
+ },
+ ),
+ ],
),
),
);
app/lib/features/old_bluetooth/bluetooth_input.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:blood_pressure_app/config.dart';
import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart' show BluetoothInput;
import 'package:blood_pressure_app/features/old_bluetooth/logic/ble_read_cubit.dart';
import 'package:blood_pressure_app/features/old_bluetooth/logic/bluetooth_cubit.dart';
@@ -23,25 +24,13 @@ import 'package:health_data_store/health_data_store.dart';
/// This widget is superseded by [BluetoothInput].
class OldBluetoothInput extends StatefulWidget {
/// Create a measurement input through bluetooth.
- const OldBluetoothInput({super.key,
+ OldBluetoothInput({super.key,
required this.onMeasurement,
- this.bluetoothCubit,
- this.deviceScanCubit,
- this.bleReadCubit,
- });
+ }) : assert(!isTestingEnvironment, "OldBluetoothInput isn't maintained in tests");
/// Called when a measurement was received through bluetooth.
final void Function(BloodPressureRecord data) onMeasurement;
- /// Function to customize [BluetoothCubit] creation.
- final BluetoothCubit Function()? bluetoothCubit;
-
- /// Function to customize [DeviceScanCubit] creation.
- final DeviceScanCubit Function()? deviceScanCubit;
-
- /// Function to customize [BleReadCubit] creation.
- final BleReadCubit Function(BluetoothDevice dev)? bleReadCubit;
-
@override
State<OldBluetoothInput> createState() => _OldBluetoothInputState();
}
@@ -64,7 +53,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
@override
void initState() {
super.initState();
- _bluetoothCubit = widget.bluetoothCubit?.call() ?? BluetoothCubit();
+ _bluetoothCubit = BluetoothCubit();
}
@override
@@ -103,7 +92,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
}
});
final settings = context.watch<Settings>();
- _deviceScanCubit ??= widget.deviceScanCubit?.call() ?? DeviceScanCubit(
+ _deviceScanCubit ??= DeviceScanCubit(
service: serviceUUID,
settings: settings,
);
@@ -128,7 +117,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
// distinction
DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
bloc: () {
- _deviceReadCubit = widget.bleReadCubit?.call(state.device) ?? BleReadCubit(
+ _deviceReadCubit = BleReadCubit(
state.device,
characteristicUUID: characteristicUUID,
serviceUUID: serviceUUID,
app/lib/l10n/app_en.arb
@@ -297,7 +297,7 @@
"@last30Days": {},
"allowMissingValues": "Allow missing values",
"@allowMissingValues": {},
- "errTimeAfterNow": "The selected time of day was reset, as it occurs after this moment. You can turn off this validation in the settings.",
+ "errTimeAfterNow": "The selected time is in the future. You can turn off this validation in the settings.",
"@errTimeAfterNow": {},
"language": "Language",
"@language": {},
@@ -552,5 +552,8 @@
"newBluetoothInputCrossPlatform": "Beta cross-platform",
"@newBluetoothInputCrossPlatform": {},
"bluetoothInputDesc": "The beta backend works on more devices but is less tested. The cross-platform version may work on non-android and is planned to supersede the stable implementation once mature enough.",
- "@bluetoothInputDesc": {}
+ "@bluetoothInputDesc": {},
+ "@bluetoothInputDesc": {},
+ "tapToSelect": "Tap to select",
+ "@tapToSelect": {}
}
app/lib/model/storage/settings_store.dart
@@ -411,7 +411,7 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
- BluetoothInputMode _bleInput = BluetoothInputMode.oldBluetoothInput;
+ BluetoothInputMode _bleInput = BluetoothInputMode.disabled;
/// Whether to show bluetooth input on add measurement page.
BluetoothInputMode get bleInput => _bleInput;
set bleInput(BluetoothInputMode value) {
app/test/features/input/forms/add_entry_form_test.dart
@@ -0,0 +1,431 @@
+import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.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:intl/intl.dart';
+
+import '../../../model/analyzer_test.dart';
+import '../../../util.dart';
+import '../../measurement_list/measurement_list_entry_test.dart';
+
+void main() {
+ group('shows sub-forms depending on settings', () {
+ // always show NoteForm, BloodPressureForm
+
+ testWidgets('show TimeForm if and only if setting is set (default true)', (tester) async {
+ final settings = Settings();
+ await tester.pumpWidget(materialApp(AddEntryForm(meds: []), settings: settings));
+
+ expect(find.byType(TabBar, skipOffstage: false), findsNothing);
+ expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+ expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+
+ settings.allowManualTimeInput = false;
+ await tester.pumpAndSettle();
+
+ expect(find.byType(DateTimeForm, skipOffstage: false), findsNothing);
+ });
+
+ testWidgets('show WeightForm if and only if setting is set', (tester) async {
+ final settings = Settings(weightInput: true);
+ await tester.pumpWidget(materialApp(AddEntryForm(meds: []), settings: settings));
+
+ expect(find.byType(TabBar, skipOffstage: false), findsOneWidget);
+ expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(WeightForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+
+ settings.weightInput = false;
+ await tester.pumpAndSettle();
+
+ expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+ });
+
+ testWidgets('show MedicineIntakeForm if medicines are available', (tester) async {
+ await tester.pumpWidget(materialApp(AddEntryForm(meds: [mockMedicine()])));
+
+ expect(find.byType(TabBar, skipOffstage: false), findsOneWidget);
+ expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+ expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+ });
+
+ testWidgets('show the BluetoothInput specified by setting', (tester) async {
+ final settings = Settings(bleInput: BluetoothInputMode.disabled);
+ await tester.pumpWidget(materialApp(AddEntryForm(
+ bluetoothCubit: _MockBluetoothCubit.new
+ ), settings: settings));
+
+ expect(find.byType(TabBar, skipOffstage: false), findsNothing);
+ expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+ expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+ expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+
+ settings.bleInput = BluetoothInputMode.newBluetoothInputOldLib;
+ await tester.pumpAndSettle();
+
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsOneWidget);
+
+ settings.bleInput = BluetoothInputMode.disabled;
+ await tester.pumpAndSettle();
+
+ expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+ expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+ });
+ });
+
+ testWidgets('saves all entered values', (tester) async {
+ final med1 = mockMedicine(color: Colors.blue, designation: 'med123', defaultDosis: 3.14);
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med1]),
+ settings: Settings(weightInput: true)
+ ));
+
+ final fields = find.byType(TextField);
+ await tester.enterText(fields.at(0), '123'); // sys
+ await tester.enterText(fields.at(1), '45'); // dia
+ await tester.enterText(fields.at(2), '67'); // pul
+
+ await tester.tap(find.byIcon(Icons.medication_outlined));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text(med1.designation)); // med
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byIcon(Icons.scale));
+ await tester.pumpAndSettle();
+ await tester.enterText(find.byType(TextField).first, '65.4'); // weight
+
+ await tester.enterText(find.descendant(
+ of: find.byType(NoteForm),
+ matching: find.byType(TextField),
+ ), 'some note'); // note text
+ await tester.pumpAndSettle();
+
+ expect(key.currentState!.validate(), true);
+ final res = key.currentState!.save();
+ expect(res?.record?.sys?.mmHg, 123);
+ expect(res?.record?.dia?.mmHg, 45);
+ expect(res?.record?.pul, 67);
+ expect(res?.intake?.medicine, med1);
+ expect(res?.intake?.dosis, med1.dosis);
+ expect(res?.weight?.weight.kg, 65.4);
+ expect(res?.note?.note, 'some note');
+ expect(res?.note?.color, isNull);
+ });
+
+ testWidgets('saves partially entered values (blood pressure)', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+
+ final fields = find.byType(TextField);
+ await tester.enterText(fields.at(0), '123'); // sys
+ await tester.enterText(fields.at(1), '45'); // dia
+ await tester.enterText(fields.at(2), '67'); // pul
+
+ expect(key.currentState!.validate(), true);
+ final res = key.currentState!.save();
+ expect(res?.record?.sys?.mmHg, 123);
+ expect(res?.record?.dia?.mmHg, 45);
+ expect(res?.record?.pul, 67);
+ expect(res?.intake, isNull);
+ expect(res?.note, isNull);
+ });
+
+ testWidgets('saves partially entered values (note)', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+
+ await tester.enterText(find.descendant(
+ of: find.byType(NoteForm),
+ matching: find.byType(TextField),
+ ), 'some note'); // note text
+ await tester.pumpAndSettle();
+
+ expect(key.currentState!.validate(), true);
+ final res = key.currentState!.save();
+ expect(res?.record, isNull);
+ expect(res?.intake, isNull);
+ expect(res?.weight, isNull);
+ expect(res?.note?.note, 'some note');
+ });
+
+ testWidgets('saves partially entered values (intake)', (tester) async {
+ final med1 = mockMedicine(color: Colors.blue, designation: 'med123', defaultDosis: 3.14);
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med1])));
+
+ await tester.tap(find.byIcon(Icons.medication_outlined));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text(med1.designation)); // med
+ await tester.pumpAndSettle();
+
+ expect(key.currentState!.validate(), true);
+ final res = key.currentState!.save();
+ expect(res?.record, isNull);
+ expect(res?.weight, isNull);
+ expect(res?.note, isNull);
+ expect(res?.intake?.medicine, med1);
+ expect(res?.intake?.dosis, med1.dosis);
+ });
+
+ testWidgets('initializes timestamp correctly', (tester) async {
+ final dateFormatter = DateFormat('yyyy-MM-dd');
+ final timeFormatter = DateFormat('HH:mm');
+
+ final start = DateTime.now();
+ await tester.pumpWidget(materialApp(AddEntryForm(meds: [])));
+
+ expect(find.text(dateFormatter.format(start)), findsOneWidget);
+ final allowedTimes = anyOf(timeFormatter.format(start), timeFormatter.format(start.add(Duration(minutes: 1))));
+ expect(find.byWidgetPredicate(
+ (w) => w is Text && allowedTimes.matches(w.data, {})),
+ findsOneWidget);
+ });
+
+ testWidgets('validates time form', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ final time = DateTime.now();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+ expect(key.currentState?.validate(), true);
+
+ key.currentState!.fillForm((
+ timestamp: time.add(Duration(hours: 1)),
+ intake: null, note: null, record: null, weight: null,
+ ));
+ await tester.pump();
+ expect(key.currentState?.validate(), false);
+ });
+
+ testWidgets('validates bp form', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+ expect(key.currentState?.validate(), true);
+
+ key.currentState!.fillForm((
+ timestamp: DateTime.now(),
+ record: mockRecord(sys: 123123),
+ note: null, intake: null, weight: null,
+ ));
+ await tester.pump();
+ expect(key.currentState?.validate(), false);
+ });
+
+ testWidgets('validates weight form', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key),
+ settings: Settings(weightInput: true),
+ ));
+ expect(key.currentState?.validate(), true);
+
+ await tester.tap(find.byIcon(Icons.scale));
+ await tester.pumpAndSettle();
+ await tester.enterText(find.byType(TextField).first, ',.,');
+ await tester.pump();
+ expect(key.currentState?.validate(), false);
+ });
+
+ testWidgets('validates intake form', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ final med = mockMedicine(designation: 'testmed');
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med])));
+ expect(key.currentState?.validate(), true);
+
+ await tester.tap(find.byIcon(Icons.medication_outlined));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text(med.designation));
+ await tester.pump();
+ await tester.enterText(find.byType(TextField).first, ',.,');
+ await tester.pump();
+ expect(key.currentState?.validate(), false);
+ });
+
+ testWidgets('saves initial values as is', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ final med = mockMedicine(designation: 'somemed123');
+ final intake = mockIntake(med);
+ final value = (
+ timestamp: intake.time,
+ intake: intake,
+ note: Note(time: intake.time, note: '123test', color: Colors.teal.value),
+ record: mockRecord(time: intake.time, sys: 123, dia: 45, pul: 67),
+ weight: BodyweightRecord(time: intake.time, weight: Weight.kg(123.45))
+ );
+ await tester.pumpWidget(materialApp(AddEntryForm(
+ key: key,
+ meds: [med],
+ initialValue: value,
+ )));
+ await tester.pumpAndSettle();
+
+ expect(key.currentState?.validate(), true);
+ expect(key.currentState?.save(), isA<AddEntryFormValue>()
+ .having((e) => e.timestamp, 'timestamp', value.timestamp)
+ .having((e) => e.intake, 'intake', value.intake)
+ .having((e) => e.record, 'record', value.record)
+ .having((e) => e.weight, 'weight', value.weight)
+ .having((e) => e.note, 'note', value.note));
+ });
+
+ testWidgets('saves loaded values as is', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ final med = mockMedicine(designation: 'somemed123');
+ final intake = mockIntake(med);
+ final value = (
+ timestamp: intake.time,
+ intake: intake,
+ note: Note(time: intake.time, note: '123test', color: Colors.teal.value),
+ record: mockRecord(time: intake.time, sys: 123, dia: 45, pul: 67),
+ weight: BodyweightRecord(time: intake.time, weight: Weight.kg(123.45))
+ );
+ await tester.pumpWidget(materialApp(AddEntryForm(
+ key: key,
+ meds: [med],
+ )));
+ await tester.pumpAndSettle();
+ key.currentState!.fillForm(value);
+ await tester.pumpAndSettle();
+
+ expect(key.currentState?.validate(), true);
+ expect(key.currentState?.save(), isA<AddEntryFormValue>()
+ .having((e) => e.timestamp, 'timestamp', value.timestamp)
+ .having((e) => e.intake, 'intake', value.intake)
+ .having((e) => e.record, 'record', value.record)
+ .having((e) => e.weight, 'weight', value.weight)
+ .having((e) => e.note, 'note', value.note));
+ });
+
+ testWidgets("doesn't save empty forms", (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+ await tester.pumpAndSettle();
+ expect(key.currentState!.validate(), true);
+ expect(key.currentState!.save(), isNull);
+ });
+
+ testWidgets('focuses last input field on backspace', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ final fields = find.byType(TextField);
+ await tester.enterText(fields.at(0), '123'); // sys
+ await tester.enterText(fields.at(1), '45'); // dia
+ await tester.enterText(fields.at(2), '67'); // pul
+
+ await tester.tap(find.text('67'));
+
+ Finder focusedTextField() {
+ final firstFocused = FocusManager.instance.primaryFocus;
+ expect(firstFocused?.context?.widget, isNotNull);
+ return find.ancestor(
+ of: find.byWidget(FocusManager.instance.primaryFocus!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ }
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsNothing);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsOneWidget);
+
+ await tester.enterText(focusedTextField(), '');
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.pump();
+
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsNothing);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsOneWidget);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.pump();
+
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsOneWidget);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+
+ // doesn't focus last input on backspace if the current is still filled
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+ await tester.pump();
+
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsOneWidget);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+ expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+ });
+
+ testWidgets('should allow invalid values when setting is set', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key),
+ settings: Settings(validateInputs: false)),
+ );
+
+ final fields = find.byType(TextField);
+ await tester.enterText(fields.at(0), '12'); // sys
+ await tester.enterText(fields.at(1), '450'); // dia
+ await tester.enterText(fields.at(2), '67123'); // pul
+
+ expect(key.currentState!.validate(), true);
+ final res = key.currentState!.save();
+ expect(res?.record?.sys?.mmHg, 12);
+ expect(res?.record?.dia?.mmHg, 450);
+ expect(res?.record?.pul, 67123);
+ });
+
+ testWidgets('starts with sys input focused', (tester) async {
+ final key = GlobalKey<AddEntryFormState>();
+ await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ await tester.pump();
+ expect(find.descendant(
+ of: find.ancestor(
+ of: find.byWidget(FocusManager.instance.primaryFocus!.context!.widget),
+ matching: find.byType(TextField)
+ ),
+ matching: find.text(localizations.sysLong)
+ ), findsOneWidget);
+ });
+}
+
+class _MockBluetoothCubit extends Fake implements BluetoothCubit {
+ @override
+ Future<void> close() async {}
+
+ @override
+ BluetoothState get state => BluetoothStateDisabled();
+
+ @override
+ Stream<BluetoothState> get stream => Stream.empty();
+}
app/test/features/input/forms/blood_pressure_form_test.dart
@@ -0,0 +1,89 @@
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+
+void main() {
+ testWidgets('saves entered values', (WidgetTester tester) async {
+ final key = GlobalKey<BloodPressureFormState>();
+ await tester.pumpWidget(materialApp(BloodPressureForm(key: key)));
+ await tester.pumpAndSettle();
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.sysLong), findsOneWidget);
+ expect(find.text(localizations.diaLong), findsOneWidget);
+ expect(find.text(localizations.pulLong), findsOneWidget);
+ expect(find.byType(TextField), findsNWidgets(3));
+ expect(key.currentState?.validate(), true);
+
+ await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '123');
+ await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '67');
+ await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '89');
+
+ expect(key.currentState?.validate(), true);
+ expect(key.currentState?.save(), (sys: 123, dia: 67, pul: 89));
+ });
+
+ testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+ final key = GlobalKey<BloodPressureFormState>();
+ await tester.pumpWidget(materialApp(BloodPressureForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.errNaN), findsNothing);
+
+ await tester.enterText(find.byType(TextField).first, '..,..');
+ await tester.pump();
+ expect(find.text('..,..'), findsNothing);
+
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsNothing);
+
+ await tester.enterText(find.byType(TextField).first, '13');
+ await tester.pump();
+ expect(key.currentState!.validate(), isFalse);
+ await tester.pumpAndSettle();
+
+ expect(find.text(localizations.errLt30), findsOneWidget);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+
+ await tester.enterText(find.byType(TextField).first, '500');
+ await tester.pump();
+ expect(key.currentState!.validate(), isFalse);
+ await tester.pumpAndSettle();
+
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsOneWidget);
+
+ await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '123');
+ await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '67');
+ await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '');
+ await tester.pump();
+ expect(key.currentState!.validate(), isFalse);
+ await tester.pumpAndSettle();
+
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errNaN), findsOneWidget, reason: 'pul is null');
+
+ await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '90');
+ await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '130');
+ await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '89');
+ await tester.pump();
+ expect(key.currentState!.validate(), isFalse);
+ await tester.pumpAndSettle();
+
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsOneWidget);
+ });
+
+ testWidgets('loads initial values', (WidgetTester tester) async {
+ await tester.pumpWidget(materialApp(BloodPressureForm(
+ initialValue: (sys: 123, dia: 67, pul: 89),
+ )));
+ await tester.pumpAndSettle();
+ expect(find.text('123'), findsOneWidget);
+ expect(find.text('67'), findsOneWidget);
+ expect(find.text('89'), findsOneWidget);
+ });
+}
app/test/features/input/forms/date_time_form_test.dart
@@ -0,0 +1,100 @@
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/intl.dart';
+
+import '../../../util.dart';
+
+void main() {
+ testWidgets('saves entered values', (WidgetTester tester) async {
+ final key = GlobalKey<DateTimeFormState>();
+ final initialTime = DateTime(2025,02,28,10,10);
+ await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.date), findsOneWidget);
+ expect(find.text(localizations.time), findsOneWidget);
+ expect(key.currentState!.validate(), true);
+ expect(find.byType(InputDecorator), findsNWidgets(2));
+
+ final dateFormated = DateFormat('yyyy-MM-dd').format(initialTime);
+ final timeOfDayFormated = DateFormat('HH:mm').format(initialTime);
+ expect(find.text(dateFormated), findsOneWidget);
+ expect(find.text(timeOfDayFormated), findsOneWidget);
+
+ await tester.tap(find.text(dateFormated));
+ await tester.pumpAndSettle();
+ expect(find.byType(DatePickerDialog), findsOneWidget);
+ await tester.tap(find.text('19'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('OK'));
+ await tester.pumpAndSettle();
+
+ expect(find.text(dateFormated), findsNothing);
+ expect(find.text(DateFormat('yyyy-MM-dd').format(DateTime(2025,02,19))), findsOneWidget);
+ expect(find.text(timeOfDayFormated), findsOneWidget);
+
+ await tester.tap(find.text(timeOfDayFormated));
+ await tester.pumpAndSettle();
+ expect(find.byType(TimePickerDialog), findsOneWidget);
+ final dialCenter = tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
+ await tester.tapAt(Offset(dialCenter.dx, dialCenter.dy + 10)); // 6 AM
+ await tester.pumpAndSettle();
+ await tester.tapAt(Offset(dialCenter.dx - 10, dialCenter.dy)); // 45
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('OK'));
+ await tester.pumpAndSettle();
+
+ expect(find.text(DateFormat('yyyy-MM-dd').format(DateTime(2025,02,19))), findsOneWidget);
+ expect(find.text(timeOfDayFormated), findsNothing);
+ expect(find.text(DateFormat('HH:mm').format(DateTime(2025,2,19,6,45))), findsOneWidget);
+
+ expect(key.currentState!.validate(), true);
+ expect(key.currentState!.save(), DateTime(2025,2,19,6,45));
+ });
+
+ testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+ final key = GlobalKey<DateTimeFormState>();
+ final initialTime = DateTime.now();
+ await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.errTimeAfterNow), findsNothing);
+
+ await tester.tap(find.text(DateFormat('HH:mm').format(initialTime)));
+ await tester.pumpAndSettle();
+ final dialCenter = tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
+ await tester.tapAt(Offset(dialCenter.dx - 3, dialCenter.dy - 9.5));
+ await tester.tap(find.text('PM')); // 11 PM
+ await tester.pumpAndSettle();
+ await tester.tapAt(Offset(dialCenter.dx - 3, dialCenter.dy - 9.5)); // 55
+ await tester.tap(find.text('OK'));
+ await tester.pumpAndSettle();
+
+ expect(key.currentState!.save(), null);
+ expect(key.currentState!.validate(), false);
+ await tester.pumpAndSettle();
+ expect(find.text(localizations.errTimeAfterNow), findsOneWidget);
+ });
+
+ testWidgets('loads values into fields', (WidgetTester tester) async {
+ final key = GlobalKey<DateTimeFormState>();
+ final initialTime = DateTime(2025,02,28,10,10);
+ final newTime = DateTime(2022,11,1,2,3);
+ await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+
+ expect(find.text(DateFormat('yyyy-MM-dd').format(initialTime)), findsOneWidget);
+ expect(find.text(DateFormat('HH:mm').format(initialTime)), findsOneWidget);
+ expect(find.text(DateFormat('yyyy-MM-dd').format(newTime)), findsNothing);
+ expect(find.text(DateFormat('HH:mm').format(newTime)), findsNothing);
+
+ key.currentState!.fillForm(DateTime(2022,11,1,2,3));
+ await tester.pumpAndSettle();
+
+ expect(find.text(DateFormat('yyyy-MM-dd').format(initialTime)), findsNothing);
+ expect(find.text(DateFormat('HH:mm').format(initialTime)), findsNothing);
+ expect(find.text(DateFormat('yyyy-MM-dd').format(newTime)), findsOneWidget);
+ expect(find.text(DateFormat('HH:mm').format(newTime)), findsOneWidget);
+ });
+}
app/test/features/input/forms/form_switcher_test.dart
@@ -0,0 +1,72 @@
+import 'package:blood_pressure_app/features/input/forms/form_switcher.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:inline_tab_view/inline_tab_view.dart';
+
+void main() {
+ testWidgets('shows a InlineTabView', (tester) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: FormSwitcher(
+ subForms: [
+ (SizedBox(width: 1, height: 1), SizedBox(width: 2, height: 2))
+ ],
+ ))));
+
+ expect(find.byType(TabBar), findsNothing, reason: 'only one tab present');
+ expect(find.byType(TabBarView), findsNothing);
+ expect(find.byType(InlineTabView), findsNothing, reason: 'only one tab present');
+ });
+
+ testWidgets('shows all passed tabs in TabBar', (tester) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: FormSwitcher(
+ subForms: [
+ (Text('Tab 1'), SizedBox(width: 2, height: 2)),
+ (Text('Tab 2'), SizedBox(width: 2, height: 2)),
+ (Text('Tab 3'), SizedBox(width: 2, height: 2)),
+ (Text('Tab 4'), SizedBox(width: 2, height: 2)),
+ ],
+ ))));
+
+ expect(find.text('Tab 1'), findsOneWidget);
+ expect(find.text('Tab 2'), findsOneWidget);
+ expect(find.text('Tab 3'), findsOneWidget);
+ expect(find.text('Tab 4'), findsOneWidget);
+ });
+
+ testWidgets('associates title and widget correctly', (tester) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: FormSwitcher(
+ subForms: [
+ (Text('Tab 1'), Text('Content 1')),
+ (Text('Tab 2'), Text('Content 2')),
+ (Text('Tab 3'), Text('Content 3')),
+ (Text('Tab 4'), Text('Content 4')),
+ ],
+ ))));
+
+ expect(find.text('Content 1'), findsOneWidget);
+ expect(find.text('Content 2'), findsNothing);
+ expect(find.text('Content 3'), findsNothing);
+ expect(find.text('Content 4'), findsNothing);
+
+ await tester.tap(find.text('Tab 2'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Content 1'), findsNothing);
+ expect(find.text('Content 2'), findsOneWidget);
+ expect(find.text('Content 3'), findsNothing);
+ expect(find.text('Content 4'), findsNothing);
+
+ await tester.tap(find.text('Tab 4'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Content 1'), findsNothing);
+ expect(find.text('Content 2'), findsNothing);
+ expect(find.text('Content 3'), findsNothing);
+ expect(find.text('Content 4'), findsOneWidget);
+ });
+}
app/test/features/input/forms/medicine_intake_form_test.dart
@@ -0,0 +1,114 @@
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:flutter/material.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 '../../../util.dart';
+
+void main() {
+ testWidgets('shows input list on single med', (WidgetTester tester) async {
+ final mockMed = mockMedicine(designation: 'monozernorditrocin');
+ await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [mockMed])));
+ final localizations = await AppLocalizations.delegate.load(Locale('en'));
+
+ expect(find.byType(TextField), findsNothing);
+ expect(find.text('monozernorditrocin'), findsOneWidget);
+ expect(find.text(localizations.tapToSelect), findsOneWidget);
+ });
+
+ testWidgets('shows input list on multiple meds', (WidgetTester tester) async {
+ final med1 = mockMedicine(designation: 'tetraebraphthyme');
+ final med2 = mockMedicine(designation: 'hypovonyhensas');
+ await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [med1,med2])));
+ final localizations = await AppLocalizations.delegate.load(Locale('en'));
+
+ expect(find.text('tetraebraphthyme'), findsOneWidget);
+ expect(find.text('hypovonyhensas'), findsOneWidget);
+ expect(find.byType(TextField), findsNothing);
+ expect(find.byIcon(Icons.close), findsNothing);
+ expect(find.text(localizations.tapToSelect), findsNothing);
+
+ await tester.tap(find.text('hypovonyhensas'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('tetraebraphthyme'), findsNothing);
+ expect(find.text('hypovonyhensas'), findsOneWidget);
+ expect(find.byType(TextField), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+
+ await tester.tap(find.byIcon(Icons.close));
+ await tester.pumpAndSettle();
+
+ expect(find.text('tetraebraphthyme'), findsOneWidget);
+ expect(find.text('hypovonyhensas'), findsOneWidget);
+ expect(find.byType(TextField), findsNothing);
+ expect(find.byIcon(Icons.close), findsNothing);
+
+ await tester.tap(find.text('tetraebraphthyme'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('tetraebraphthyme'), findsOneWidget);
+ expect(find.text('hypovonyhensas'), findsNothing);
+ expect(find.byType(TextField), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ });
+
+ testWidgets('returns entered values', (WidgetTester tester) async {
+ final med1 = mockMedicine(designation: 'tetraebraphthyme');
+ final med2 = mockMedicine(designation: 'hypovonyhensas');
+ final key = GlobalKey<MedicineIntakeFormState>();
+
+ await tester.pumpWidget(materialApp(MedicineIntakeForm(
+ meds: [med1,med2],
+ key: key,
+ )));
+
+ expect(key.currentState!.validate(), isTrue);
+ expect(key.currentState!.save(), isNull);
+
+ await tester.tap(find.text(med1.designation));
+ await tester.pumpAndSettle();
+ await tester.enterText(find.byType(TextField), ',..,');
+
+ expect(key.currentState!.validate(), isFalse);
+ expect(key.currentState!.save(), isNull);
+
+ await tester.enterText(find.byType(TextField), '3.14');
+ expect(key.currentState!.validate(), isTrue);
+ expect(key.currentState!.save(), (med1, Weight.mg(3.14)));
+ });
+
+ testWidgets('prefills values when selecting med', (WidgetTester tester) async {
+ final med1 = mockMedicine(designation: 'tetraebraphthyme', defaultDosis: 3.141);
+ final med2 = mockMedicine(designation: 'hypovonyhensas');
+ await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [med1,med2])));
+
+ await tester.tap(find.text(med1.designation));
+ await tester.pumpAndSettle();
+
+ expect(find.text(med1.dosis!.mg.toString()), findsOneWidget);
+ });
+
+ testWidgets('returns passed values on edit', (WidgetTester tester) async {
+ final med1 = mockMedicine(designation: 'tetraebraphthyme');
+ final med2 = mockMedicine(designation: 'hypovonyhensas');
+ final key = GlobalKey<MedicineIntakeFormState>();
+
+ await tester.pumpWidget(materialApp(MedicineIntakeForm(
+ meds: [med1,med2],
+ key: key,
+ initialValue: (med2, Weight.mg(3.141)),
+ )));
+ await tester.pumpAndSettle();
+
+ expect(find.text(med2.designation), findsOneWidget);
+ expect(find.text('3.141'), findsOneWidget);
+ expect(find.byType(TextField), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+
+ expect(key.currentState!.validate(), isTrue);
+ expect(key.currentState!.save(), (med2, Weight.mg(3.141)));
+ });
+
+}
app/test/features/input/forms/note_form_test.dart
@@ -0,0 +1,64 @@
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+import '../../settings/tiles/color_picker_list_tile_test.dart';
+
+void main() {
+ testWidgets('saves entered text', (WidgetTester tester) async {
+ final key = GlobalKey<NoteFormState>();
+ await tester.pumpWidget(materialApp(NoteForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.addNote), findsOneWidget);
+ expect(key.currentState!.validate(), true);
+
+ await tester.enterText(find.byType(TextField), 'some test note!');
+
+ expect(key.currentState!.validate(), true);
+ expect(key.currentState!.save(), ('some test note!', null));
+ });
+
+ testWidgets('saves entered color and text', (WidgetTester tester) async {
+ final key = GlobalKey<NoteFormState>();
+ await tester.pumpWidget(materialApp(NoteForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.addNote), findsOneWidget);
+ expect(key.currentState!.validate(), true);
+
+ await tester.enterText(find.byType(TextField), 'some test note!');
+ await tester.tap(find.byType(ColorSelectionListTile));
+ await tester.pump();
+ await tester.tap(find.byElementPredicate(findColored(Colors.red)));
+
+ expect(key.currentState!.validate(), true);
+ expect(key.currentState!.save(), ('some test note!', Colors.red));
+ });
+
+ testWidgets('loads initial values', (WidgetTester tester) async {
+ await tester.pumpWidget(materialApp(NoteForm(
+ initialValue: ('Some note text from test', Colors.cyan),
+ )));
+ await tester.pumpAndSettle();
+ expect(find.text('Some note text from test'), findsOneWidget);
+ await tester.tap(find.byElementPredicate(findColored(Colors.cyan)));
+ });
+
+ testWidgets('saves only filled inputs', (WidgetTester tester) async {
+ final key = GlobalKey<NoteFormState>();
+ await tester.pumpWidget(materialApp(NoteForm(key: key)));
+ expect(key.currentState!.save(), isNull);
+ });
+
+ testWidgets('saves prefilled inputs', (WidgetTester tester) async {
+ final v = ('Some note text from test', Colors.cyan);
+
+ final key = GlobalKey<NoteFormState>();
+ await tester.pumpWidget(materialApp(NoteForm(key: key, initialValue: v)));
+ expect(key.currentState!.save(), v);
+ });
+}
app/test/features/input/forms/weight_form_test.dart
@@ -0,0 +1,51 @@
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+
+void main() {
+ testWidgets('saves entered values', (WidgetTester tester) async {
+ final key = GlobalKey<WeightFormState>();
+ await tester.pumpWidget(materialApp(WeightForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.weight), findsOneWidget);
+ expect(find.text(Settings().weightUnit.name), findsOneWidget);
+ expect(key.currentState!.validate(), true);
+
+ await tester.enterText(find.byType(TextField), '314.15');
+
+ expect(key.currentState!.validate(), true);
+ expect(key.currentState!.save(), Settings().weightUnit.store(314.15));
+ });
+
+ testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+ final key = GlobalKey<WeightFormState>();
+ await tester.pumpWidget(materialApp(WeightForm(key: key)));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.text(localizations.errNaN), findsNothing);
+
+ await tester.enterText(find.byType(TextField), '..,..');
+ expect(key.currentState!.validate(), false);
+ await tester.pumpAndSettle();
+ expect(find.text(localizations.errNaN), findsOneWidget);
+ });
+
+ testWidgets('loads initial values', (WidgetTester tester) async {
+ await tester.pumpWidget(materialApp(WeightForm(
+ initialValue: Settings().weightUnit.store(123.45),
+ )));
+ await tester.pumpAndSettle();
+ expect(find.text('123.45'), findsOneWidget);
+ });
+
+ testWidgets('saves only filled inputs', (WidgetTester tester) async {
+ final key = GlobalKey<WeightFormState>();
+ await tester.pumpWidget(materialApp(WeightForm(key: key)));
+ expect(key.currentState!.save(), isNull);
+ });
+}
app/test/features/input/add_bodyweight_dialoge_test.dart
@@ -1,68 +0,0 @@
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/model/weight_unit.dart';
-import 'package:flutter/material.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 '../../util.dart';
-
-void main() {
- testWidgets('shows weight input weight', (tester) async {
- await tester.pumpWidget(materialApp(const AddBodyweightDialoge()));
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- expect(find.byType(TextFormField), findsOneWidget);
- expect(find.text(localizations.weight), findsOneWidget);
- expect(find.text('kg'), findsOneWidget);
-
- await tester.enterText(find.byType(TextFormField), '123.45');
- });
- testWidgets('error on invalid input', (tester) async {
- await tester.pumpWidget(materialApp(const AddBodyweightDialoge()));
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- expect(find.text(localizations.errNaN), findsNothing);
-
- await tester.enterText(find.byType(TextFormField), 'invalid input');
- await tester.testTextInput.receiveAction(TextInputAction.done);
- await tester.pumpAndSettle();
-
- expect(find.text(localizations.errNaN), findsOneWidget);
- });
- testWidgets('creates weight from input', (tester) async {
- Weight? res;
- await tester.pumpWidget(materialApp(Builder(
- builder: (context) => GestureDetector(
- onTap: () async => res = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge()),
- child: const Text('X'),
- ),
- )));
- await tester.tap(find.text('X'));
- await tester.pumpAndSettle();
-
- await tester.enterText(find.byType(TextFormField), '123.45');
- await tester.testTextInput.receiveAction(TextInputAction.done);
- await tester.pumpAndSettle();
-
- expect(res, Weight.kg(123.45));
- });
- testWidgets('respects preferred weight unit', (tester) async {
- Weight? res;
- await tester.pumpWidget(materialApp(Builder(
- builder: (context) => GestureDetector(
- onTap: () async => res = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge()),
- child: const Text('X'),
- ),
- ), settings: Settings(weightUnit: WeightUnit.st)));
- await tester.tap(find.text('X'));
- await tester.pumpAndSettle();
-
- await tester.enterText(find.byType(TextFormField), '123.45');
- await tester.testTextInput.receiveAction(TextInputAction.done);
- await tester.pumpAndSettle();
-
- expect(res, WeightUnit.st.store(123.45));
- });
-}
app/test/features/input/add_entry_dialogue_test.dart
@@ -0,0 +1,209 @@
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.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 '../../model/export_import/record_formatter_test.dart';
+import '../../util.dart';
+
+void main() {
+ testWidgets('respects bottomAppBars', (tester) async {
+ final settings = Settings(bottomAppBars: false);
+ await tester.pumpWidget(materialApp(const AddEntryDialogue(),
+ settings: settings
+ ));
+ final initialHeights = tester.getCenter(find.byType(AppBar)).dy;
+
+ settings.bottomAppBars = true;
+ await tester.pump();
+
+ expect(tester.getCenter(find.byType(AppBar)).dy, greaterThan(initialHeights));
+ });
+
+ // TODO: update these old tests
+ testWidgets('should show everything on initial page', (tester) async {
+ await tester.pumpWidget(materialApp(const AddEntryDialogue()));
+ expect(tester.takeException(), isNull);
+
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+ expect(find.text('SAVE'), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ expect(find.text('Systolic'), findsWidgets);
+ expect(find.text('Diastolic'), findsWidgets);
+ expect(find.text('Pulse'), findsWidgets);
+ expect(find.byType(ColorSelectionListTile), findsOneWidget);
+ },);
+ testWidgets('should prefill initialRecord values', (tester) async {
+ await tester.pumpWidget(materialApp(
+ AddEntryDialogue(
+ initialRecord: mockEntryPos(
+ DateTime.now(), 123, 56, 43, 'Test note', Colors.teal,
+ ).asAddEntry,
+ availableMeds: const [],
+ ),
+ ),);
+ await tester.pumpAndSettle();
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+ expect(find.text('SAVE'), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ expect(find.text('Test note'), findsOneWidget);
+ expect(find.text('123'), findsOneWidget);
+ expect(find.text('56'), findsOneWidget);
+ expect(find.text('43'), findsOneWidget);
+ expect(find.byType(ColorSelectionListTile), findsOneWidget);
+ tester.widget<ColorSelectionListTile>(find.byType(ColorSelectionListTile)).initialColor == Colors.teal;
+ });
+ testWidgets('should return null on cancel', (tester) async {
+ dynamic result = 'result before save';
+ await loadDialoge(tester, (context) async
+ => result = await showAddEntryDialogue(context,
+ medRepo(),
+ mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal).asAddEntry));
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ await tester.tap(find.byIcon(Icons.close));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsNothing);
+
+ expect(result, null);
+ });
+ testWidgets('should not allow invalid values', (tester) async {
+ final mRep = medRepo();
+ await loadDialoge(tester, (context) => showAddEntryDialogue(context, mRep));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+ await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '67');
+
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ expect(find.text(localizations.errNaN), findsOneWidget);
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+ await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '20');
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsOneWidget);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+ await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '60');
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '500');
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsOneWidget);
+ expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '100');
+ await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '90');
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsOneWidget);
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsOneWidget);
+
+
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '78');
+ await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsNothing);
+ expect(find.text(localizations.errNaN), findsNothing);
+ expect(find.text(localizations.errLt30), findsNothing);
+ expect(find.text(localizations.errUnrealistic), findsNothing);
+ expect(find.text(localizations.errDiaGtSys), findsNothing);
+ });
+ testWidgets('should allow invalid values when setting is set', (tester) async {
+ final mRep = medRepo();
+ await loadDialoge(tester, (context) => showAddEntryDialogue(context, mRep),
+ settings: Settings(validateInputs: false, allowMissingValues: true),
+ );
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+ await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '2');
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '500');
+ await tester.tap(find.text('SAVE'));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialogue), findsNothing);
+ });
+ testWidgets('should start with sys input focused', (tester) async {
+ final mRep = medRepo();
+ await loadDialoge(tester, (context) =>
+ showAddEntryDialogue(context, mRep, mockEntry(sys: 12).asAddEntry));
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+ final primaryFocus = FocusManager.instance.primaryFocus;
+ expect(primaryFocus?.context?.widget, isNotNull);
+ final focusedTextField = find.ancestor(
+ of: find.byWidget(primaryFocus!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ expect(focusedTextField, findsOneWidget);
+ final field = tester.widget<TextField>(focusedTextField);
+ expect(field.controller?.text, '12');
+ });
+ testWidgets('should focus next on input finished', (tester) async {
+ final mRep = medRepo();
+ await loadDialoge(tester, (context) =>
+ showAddEntryDialogue(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note').asAddEntry),);
+ expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+ await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+
+ final firstFocused = FocusManager.instance.primaryFocus;
+ expect(firstFocused?.context?.widget, isNotNull);
+ final focusedTextField = find.ancestor(
+ of: find.byWidget(firstFocused!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ expect(focusedTextField, findsOneWidget);
+ expect(focusedTextField.evaluate().first.widget, isA<TextField>()
+ .having((p0) => p0.controller?.text, 'diastolic content', '3'),);
+
+ await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '78');
+
+ final secondFocused = FocusManager.instance.primaryFocus;
+ expect(secondFocused?.context?.widget, isNotNull);
+ final secondFocusedTextField = find.ancestor(
+ of: find.byWidget(secondFocused!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ expect(secondFocusedTextField, findsOneWidget);
+ expect(secondFocusedTextField.evaluate().first.widget, isA<TextField>()
+ .having((p0) => p0.controller?.text, 'pulse content', '4'),);
+
+ await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '60');
+
+ final thirdFocused = FocusManager.instance.primaryFocus;
+ expect(thirdFocused?.context?.widget, isNotNull);
+ final thirdFocusedTextField = find.ancestor(
+ of: find.byWidget(thirdFocused!.context!.widget),
+ matching: find.byType(TextField),
+ );
+ expect(thirdFocusedTextField, findsOneWidget);
+ expect(find.descendant(of: thirdFocusedTextField, matching: find.text('Note (optional)')), findsOneWidget);
+ });
+}
app/test/features/input/add_measurement_dialoge_test.dart
@@ -1,660 +0,0 @@
-import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
-import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
-import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.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 '../../model/export_import/record_formatter_test.dart';
-import '../../util.dart';
-import '../settings/tiles/color_picker_list_tile_test.dart';
-
-void main() {
- group('AddEntryDialoge', () {
- testWidgets('should show everything on initial page', (tester) async {
- await tester.pumpWidget(materialApp(const AddEntryDialoge(availableMeds: [])));
- expect(tester.takeException(), isNull);
-
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
- expect(find.text('SAVE'), findsOneWidget);
- expect(find.byIcon(Icons.close), findsOneWidget);
- expect(find.text('Systolic'), findsWidgets);
- expect(find.text('Diastolic'), findsWidgets);
- expect(find.text('Pulse'), findsWidgets);
- expect(find.byType(ColorSelectionListTile), findsOneWidget);
- },);
- testWidgets('should prefill initialRecord values', (tester) async {
- await tester.pumpWidget(materialApp(
- AddEntryDialoge(
- initialRecord: mockEntryPos(
- DateTime.now(), 123, 56, 43, 'Test note', Colors.teal,
- ),
- availableMeds: const [],
- ),
- ),);
- await tester.pumpAndSettle();
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
- expect(find.text('SAVE'), findsOneWidget);
- expect(find.byIcon(Icons.close), findsOneWidget);
- expect(find.text('Test note'), findsOneWidget);
- expect(find.text('123'), findsOneWidget);
- expect(find.text('56'), findsOneWidget);
- expect(find.text('43'), findsOneWidget);
- expect(find.byType(ColorSelectionListTile), findsOneWidget);
- tester.widget<ColorSelectionListTile>(find.byType(ColorSelectionListTile)).initialColor == Colors.teal;
- });
- testWidgets('should show medication picker when medications available', (tester) async {
- await tester.pumpWidget(materialApp(
- AddEntryDialoge(
- availableMeds: [ mockMedicine(designation: 'testmed') ],
- ),
- ),);
- await tester.pumpAndSettle();
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- expect(find.byType(DropdownButton<Medicine?>), findsOneWidget);
- expect(find.text(localizations.noMedication), findsOneWidget);
- expect(find.text('testmed'), findsNothing);
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- expect(find.text('testmed'), findsOneWidget);
- });
- testWidgets('should reveal dosis on medication selection', (tester) async {
- await tester.pumpWidget(materialApp(
- AddEntryDialoge(
- availableMeds: [ mockMedicine(designation: 'testmed') ],
- ),
- ),);
- await tester.pumpAndSettle();
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- expect(find.byType(DropdownButton<Medicine?>), findsOneWidget);
- expect(find.text(localizations.noMedication), findsOneWidget);
- expect(find.text('testmed'), findsNothing);
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- expect(find.text(localizations.dosis), findsNothing);
- expect(find.text('testmed'), findsOneWidget);
- await tester.tap(find.text('testmed'));
- await tester.pumpAndSettle();
-
- expect(
- find.ancestor(
- of: find.text(localizations.dosis,).first,
- matching: find.byType(TextFormField),
- ),
- findsOneWidget,
- );
- });
- testWidgets('should enter default dosis if available', (tester) async {
- await tester.pumpWidget(materialApp(
- AddEntryDialoge(
- availableMeds: [ mockMedicine(designation: 'testmed', defaultDosis: 3.1415) ],
- ),
- ),);
- await tester.pumpAndSettle();
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- await tester.tap(find.text('testmed'));
- await tester.pumpAndSettle();
-
- expect(find.text('3.1415'), findsOneWidget);
- });
- testWidgets('should not quit when the measurement field is incorrectly filled, but a intake is added', (tester) async {
- await tester.pumpWidget(materialApp(
- AddEntryDialoge(
- availableMeds: [ mockMedicine(designation: 'testmed', defaultDosis: 3.1415) ],
- ),
- ),);
- await tester.pumpAndSettle();
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- await tester.tap(find.text('testmed'));
- await tester.pumpAndSettle();
-
- await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '900');
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '89');
- await tester.pumpAndSettle();
-
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errUnrealistic), findsNothing);
-
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errUnrealistic), findsOneWidget);
- });
- testWidgets('respects settings about showing bluetooth input', (tester) async {
- final settings = Settings(
- bleInput: BluetoothInputMode.newBluetoothInputCrossPlatform,
- );
- await tester.pumpWidget(materialApp(
- const AddEntryDialoge(
- availableMeds: [],
- ),
- settings: settings,
- ),);
- await tester.pumpAndSettle();
- expect(find.byType(BluetoothInput, skipOffstage: false), findsOneWidget);
-
- settings.bleInput = BluetoothInputMode.disabled;
- await tester.pumpAndSettle();
- expect(find.byType(BluetoothInput), findsNothing);
- });
- }, skip: true);
- group('showAddEntryDialoge', () {
- testWidgets('should return null on cancel', (tester) async {
- dynamic result = 'result before save';
- await loadDialoge(tester, (context) async
- => result = await showAddEntryDialoge(context,
- medRepo(),
- mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal),),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- await tester.tap(find.byIcon(Icons.close));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
-
- expect(result, null);
- });
- testWidgets('should return values on edit cancel', (tester) async {
- dynamic result = 'result before save';
- final record = mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal);
- await loadDialoge(tester, (context) async {
- result = await showAddEntryDialoge(context, medRepo(), record);
- },);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.time, record.time);
- expect(res.sys, record.sys);
- expect(res.dia, record.dia);
- expect(res.pul, record.pul);
- expect(res.note, record.note);
- expect(res.color, record.color);
- });
- testWidgets('should be able to input records', (WidgetTester tester) async {
- dynamic result = 'result before save';
- await loadDialoge(tester, (context) async {
- result = await showAddEntryDialoge(context, 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');
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '67');
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '89');
- await tester.enterText(find.ancestor(of: find.text('Note (optional)').first, matching: find.byType(TextFormField)), 'Test note');
-
- await tester.tap(find.byType(ColorSelectionListTile));
- await tester.pumpAndSettle();
- await tester.tap(find.byElementPredicate(findColored(Colors.red)));
- await tester.pumpAndSettle();
-
- expect(find.text('SAVE'), findsOneWidget);
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.sys?.mmHg, 123);
- expect(res.dia?.mmHg, 67);
- expect(res.pul, 89);
- expect(res.note, 'Test note');
- expect(res.color, Colors.red.value);
- });
- testWidgets('should allow value only', (WidgetTester tester) async {
- dynamic result = 'result before save';
- await loadDialoge(tester, (context) async
- => result = await showAddEntryDialoge(context, medRepo(),),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- await tester.enterText(find.ancestor(of: find.text(localizations.sysLong).first,
- matching: find.byType(TextFormField),), '123',);
- await tester.enterText(find.ancestor(of: find.text(localizations.diaLong).first,
- matching: find.byType(TextFormField),), '67',);
- await tester.enterText(find.ancestor(of: find.text(localizations.pulLong).first,
- matching: find.byType(TextFormField),), '89',);
-
- expect(find.text(localizations.btnSave), findsOneWidget);
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.sys?.mmHg, 123);
- expect(res.dia?.mmHg, 67);
- expect(res.pul, 89);
- expect(res.note, null);
- expect(res.color, null);
- });
- testWidgets('should allow note only', (WidgetTester tester) async {
- dynamic result = 'result before save';
- await loadDialoge(tester, (context) async
- => result = await showAddEntryDialoge(context, medRepo(),),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
- await tester.enterText(find.ancestor(of: find.text(localizations.addNote).first,
- matching: find.byType(TextFormField),), 'test note',);
-
- expect(find.text(localizations.btnSave), findsOneWidget);
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>()
- .having((p0) => p0.sys, 'systolic', null)
- .having((p0) => p0.dia, 'diastolic', null)
- .having((p0) => p0.pul, 'pulse', null)
- .having((p0) => p0.note, 'note', 'test note')
- .having((p0) => p0.color, 'needlePin', null),
- );
- });
- testWidgets('should be able to input medicines', (WidgetTester tester) async {
- final med2 = mockMedicine(designation: 'medication2', defaultDosis: 31.415);
-
- dynamic result = 'result before save';
- await loadDialoge(tester, (context) async
- => result = await showAddEntryDialoge(context, medRepo([
- mockMedicine(designation: 'medication1'),
- med2,
- ],),),);
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- final openDialogeTimeStamp = DateTime.now();
- await tester.pumpAndSettle();
-
- expect(find.text('medication1'), findsOneWidget);
- expect(find.text('medication2'), findsOneWidget);
- await tester.tap(find.text('medication2'));
- await tester.pumpAndSettle();
-
- await tester.enterText(
- find.ancestor(
- of: find.text(localizations.dosis).first,
- matching: find.byType(TextFormField),
- ),
- '123.456',
- );
-
- expect(find.text(localizations.btnSave), findsOneWidget);
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.time.millisecondsSinceEpoch, inInclusiveRange(
- openDialogeTimeStamp.millisecondsSinceEpoch - 2000,
- openDialogeTimeStamp.millisecondsSinceEpoch + 2000)
- );
- expect(res.sys, null);
- expect(res.dia, null);
- expect(res.pul, null);
- expect(res.note, null);
- expect(res.color, null);
- expect(res.intakes, hasLength(1));
- expect(res.intakes.first.medicine, med2);
- expect(res.intakes.first.dosis.mg, 123.456);
- });
- testWidgets('should not allow invalid values', (tester) async {
- final mRep = medRepo();
- await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep));
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errNaN), findsNothing);
- expect(find.text(localizations.errLt30), findsNothing);
- expect(find.text(localizations.errUnrealistic), findsNothing);
- expect(find.text(localizations.errDiaGtSys), findsNothing);
-
- await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '67');
-
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errNaN), findsOneWidget);
- expect(find.text(localizations.errLt30), findsNothing);
- expect(find.text(localizations.errUnrealistic), findsNothing);
- expect(find.text(localizations.errDiaGtSys), findsNothing);
-
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '20');
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errNaN), findsNothing);
- expect(find.text(localizations.errLt30), findsOneWidget);
- expect(find.text(localizations.errUnrealistic), findsNothing);
- expect(find.text(localizations.errDiaGtSys), findsNothing);
-
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '60');
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '500');
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errNaN), findsNothing);
- expect(find.text(localizations.errLt30), findsNothing);
- expect(find.text(localizations.errUnrealistic), findsOneWidget);
- expect(find.text(localizations.errDiaGtSys), findsNothing);
-
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '100');
- await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '90');
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsOneWidget);
- expect(find.text(localizations.errNaN), findsNothing);
- expect(find.text(localizations.errLt30), findsNothing);
- expect(find.text(localizations.errUnrealistic), findsNothing);
- expect(find.text(localizations.errDiaGtSys), findsOneWidget);
-
-
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '78');
- await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
- expect(find.text(localizations.errNaN), findsNothing);
- expect(find.text(localizations.errLt30), findsNothing);
- expect(find.text(localizations.errUnrealistic), findsNothing);
- expect(find.text(localizations.errDiaGtSys), findsNothing);
- });
- testWidgets('should allow invalid values when setting is set', (tester) async {
- final mRep = medRepo();
- await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep),
- settings: Settings(validateInputs: false, allowMissingValues: true),
- );
- 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)), '2');
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '500');
- await tester.tap(find.text('SAVE'));
- await tester.pumpAndSettle();
- expect(find.byType(AddEntryDialoge), findsNothing);
- });
- testWidgets('should respect settings.allowManualTimeInput', (tester) async {
- final mRep = medRepo();
- await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep),
- settings: Settings(allowManualTimeInput: false),
- );
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- expect(find.byIcon(Icons.edit), findsNothing);
- });
- testWidgets('should start with sys input focused', (tester) async {
- final mRep = medRepo();
- await loadDialoge(tester, (context) =>
- showAddEntryDialoge(context, mRep, mockEntry(sys: 12)),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- final primaryFocus = FocusManager.instance.primaryFocus;
- expect(primaryFocus?.context?.widget, isNotNull);
- final focusedTextFormField = find.ancestor(
- of: find.byWidget(primaryFocus!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(focusedTextFormField, findsOneWidget);
- final field = tester.widget<TextFormField>(focusedTextFormField);
- expect(field.initialValue, '12');
- });
- testWidgets('should focus next on input finished', (tester) async {
- final mRep = medRepo();
- await loadDialoge(tester, (context) =>
- showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
- 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');
-
- final firstFocused = FocusManager.instance.primaryFocus;
- expect(firstFocused?.context?.widget, isNotNull);
- final focusedTextFormField = find.ancestor(
- of: find.byWidget(firstFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(focusedTextFormField, findsOneWidget);
- expect(focusedTextFormField.evaluate().first.widget, isA<TextFormField>()
- .having((p0) => p0.initialValue, 'diastolic content', '3'),);
-
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '78');
-
- final secondFocused = FocusManager.instance.primaryFocus;
- expect(secondFocused?.context?.widget, isNotNull);
- final secondFocusedTextFormField = find.ancestor(
- of: find.byWidget(secondFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(secondFocusedTextFormField, findsOneWidget);
- expect(secondFocusedTextFormField.evaluate().first.widget, isA<TextFormField>()
- .having((p0) => p0.initialValue, 'pulse content', '4'),);
-
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '60');
-
- final thirdFocused = FocusManager.instance.primaryFocus;
- expect(thirdFocused?.context?.widget, isNotNull);
- final thirdFocusedTextFormField = find.ancestor(
- of: find.byWidget(thirdFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(thirdFocusedTextFormField, findsOneWidget);
- 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 {
- final mRep = medRepo();
- await loadDialoge(tester, (context) =>
- showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- await tester.enterText(find.ancestor(of: find.text('note').first, matching: find.byType(TextFormField)), '');
- await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
- final firstFocused = FocusManager.instance.primaryFocus;
- expect(firstFocused?.context?.widget, isNotNull);
- final focusedTextFormField = find.ancestor(
- of: find.byWidget(firstFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(focusedTextFormField, findsOneWidget);
- expect(find.descendant(of: focusedTextFormField, matching: find.text('Note (optional)')), findsNothing);
- expect(find.descendant(of: focusedTextFormField, matching: find.text('Pulse')), findsWidgets);
-
-
- await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '');
- await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
- final secondFocused = FocusManager.instance.primaryFocus;
- expect(secondFocused?.context?.widget, isNotNull);
- final secondFocusedTextFormField = find.ancestor(
- of: find.byWidget(secondFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(secondFocusedTextFormField, findsOneWidget);
- expect(find.descendant(of: secondFocusedTextFormField, matching: find.text('Pulse')), findsNothing);
- expect(find.descendant(of: secondFocusedTextFormField, matching: find.text('Diastolic')), findsWidgets);
-
-
- await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '');
- await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
- final thirdFocused = FocusManager.instance.primaryFocus;
- expect(thirdFocused?.context?.widget, isNotNull);
- final thirdFocusedTextFormField = find.ancestor(
- of: find.byWidget(thirdFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(thirdFocusedTextFormField, findsOneWidget);
- expect(find.descendant(of: thirdFocusedTextFormField, matching: find.text('Diastolic')), findsNothing);
- expect(find.descendant(of: thirdFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
-
-
- // should not go back further than systolic
- await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '');
- await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
- final fourthFocused = FocusManager.instance.primaryFocus;
- expect(fourthFocused?.context?.widget, isNotNull);
- final fourthFocusedTextFormField = find.ancestor(
- of: find.byWidget(fourthFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(fourthFocusedTextFormField, findsOneWidget);
- expect(find.descendant(of: fourthFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
- });
- testWidgets('should allow entering custom dosis', (tester) async {
- final mRep = medRepo([mockMedicine(designation: 'testmed')]);
- dynamic result;
- await loadDialoge(tester, (context) async =>
- result = await showAddEntryDialoge(context, mRep),
- );
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- await tester.tap(find.text('testmed'));
- await tester.pumpAndSettle();
-
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(find.text(localizations.errNaN), findsOneWidget);
-
- await tester.enterText(
- find.ancestor(
- of: find.text(localizations.dosis).first,
- matching: find.byType(TextFormField),
- ),
- '654.321',
- );
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.sys, null);
- expect(res.dia, null);
- expect(res.pul, null);
- expect(res.note, null);
- expect(res.color, null);
-
- expect(res.intakes, hasLength(1));
- expect(res.intakes.first.dosis.mg, 654.321);
- });
- testWidgets('should allow modifying entered dosis', (tester) async {
- final mRep = medRepo([mockMedicine(designation: 'testmed')]);
- dynamic result;
- await loadDialoge(tester, (context) async =>
- result = await showAddEntryDialoge(context, mRep),
- );
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
- await tester.tap(find.byType(DropdownButton<Medicine?>));
- await tester.pumpAndSettle();
-
- await tester.tap(find.text('testmed'));
- await tester.pumpAndSettle();
-
- await tester.enterText(
- find.ancestor(
- of: find.text(localizations.dosis).first,
- matching: find.byType(TextFormField),
- ),
- '654.321',
- );
- await tester.pumpAndSettle();
- await tester.enterText(
- find.ancestor(
- of: find.text(localizations.dosis).first,
- matching: find.byType(TextFormField),
- ),
- '654.322',
- );
- await tester.pumpAndSettle();
-
- await tester.tap(find.text(localizations.btnSave));
- await tester.pumpAndSettle();
-
- expect(result, isA<FullEntry>());
- final FullEntry res = result;
- expect(res.sys, null);
- expect(res.dia, null);
- expect(res.pul, null);
- expect(res.note, null);
- expect(res.color, null);
-
- expect(res.intakes, hasLength(1));
- expect(res.intakes.first.dosis.mg, 654.322);
- });
- testWidgets('should not go back to last field when the current field is still filled', (tester) async {
- final mRep = medRepo([mockMedicine(designation: 'testmed')]);
- await loadDialoge(tester, (context) =>
- showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
- expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
- await tester.enterText(find.ancestor(
- of: find.text('note').first,
- matching: find.byType(TextFormField),),
- 'not empty',);
- await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
- final firstFocused = FocusManager.instance.primaryFocus;
- expect(firstFocused?.context?.widget, isNotNull);
- final focusedTextFormField = find.ancestor(
- of: find.byWidget(firstFocused!.context!.widget),
- matching: find.byType(TextFormField),
- );
- expect(focusedTextFormField, findsOneWidget);
- expect(find.descendant(of: focusedTextFormField, matching: find.text('Pulse')), findsNothing);
- expect(find.descendant(of: focusedTextFormField, matching: find.text('Note (optional)')), findsWidgets);
- });
- testWidgets('opens weight input if necessary', (tester) async {
- final repo = MockBodyweightRepository();
- await tester.pumpWidget(appBase(Builder(
- builder: (context) => TextButton(onPressed: () => showAddEntryDialoge(context, MockMedRepo([])), child: const Text('X'))
- ), settings: Settings(weightInput: true), weightRepo: repo));
- final localizations = await AppLocalizations.delegate.load(const Locale('en'));
- await tester.tap(find.text('X'));
- await tester.pumpAndSettle();
-
- expect(find.text(localizations.enterWeight), findsOneWidget);
- expect(find.byIcon(Icons.scale), findsOneWidget);
- await tester.tap(find.text(localizations.enterWeight));
- await tester.pumpAndSettle();
-
- expect(repo.data, isEmpty);
- await tester.enterText(find.descendant(
- of: find.byType(AddBodyweightDialoge),
- matching: find.byType(TextFormField)
- ), '123.45');
- await tester.testTextInput.receiveAction(TextInputAction.done);
- await tester.pumpAndSettle();
- expect(repo.data, hasLength(1));
- expect(repo.data[0].weight, Weight.kg(123.45));
- });
- });
-}
app/pubspec.lock
@@ -563,6 +563,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
+ inline_tab_view:
+ dependency: "direct main"
+ description:
+ name: inline_tab_view
+ sha256: f7cc58e3253b9a9c1e45ad2eaba96ab01ce8b7fe0034e453e6e178ee97fde02d
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
integration_test:
dependency: "direct dev"
description: flutter
app/pubspec.yaml
@@ -42,6 +42,7 @@ dependencies:
# desktop only
sqflite_common_ffi: ^2.3.4+4
+ inline_tab_view: ^1.0.1
dev_dependencies:
integration_test:
health_data_store/lib/src/repositories/note_repository_impl.dart
@@ -23,7 +23,7 @@ class NoteRepositoryImpl extends NoteRepository {
Future<void> add(Note note) async {
_controller.add(null);
if (note.note == null && note.color == null) {
- assert(false);
+ assert(false, 'Attempting to store a note without content and color');
return;
}
await _db.transaction((txn) async {