Commit 56cfaca
Changed files (18)
app
lib
features
measurement_list
l10n
model
storage
test
features
measurement_list
screens
app/lib/data_util/blood_pressure_builder.dart
@@ -1,32 +0,0 @@
-import 'dart:collection';
-
-import 'package:blood_pressure_app/data_util/repository_builder.dart';
-import 'package:blood_pressure_app/model/storage/interval_store.dart';
-import 'package:collection/collection.dart';
-import 'package:flutter/material.dart';
-import 'package:health_data_store/health_data_store.dart';
-
-/// Shorthand class for getting the blood pressure values.
-class BloodPressureBuilder extends StatelessWidget {
- /// Create a loader for the measurements in the current range.
- const BloodPressureBuilder({
- super.key,
- required this.onData,
- required this.rangeType,
- });
-
- /// The build strategy once the measurement are loaded.
- final Widget Function(BuildContext context, UnmodifiableListView<BloodPressureRecord> records) onData;
-
- /// Which measurements to load.
- final IntervalStoreManagerLocation rangeType;
-
- @override
- Widget build(BuildContext context) =>
- RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
- rangeType: rangeType,
- onData: (context, List<BloodPressureRecord> data) =>
- onData(context, UnmodifiableListView(data)),
- );
-
-}
app/lib/data_util/full_entry_builder.dart
@@ -0,0 +1,44 @@
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+/// Shorthand class for getting a [rangeType]s [FullEntry] values.
+class FullEntryBuilder extends StatelessWidget {
+ /// Create a loader for getting a [rangeType]s [FullEntry] values.
+ ///
+ /// Provide either [onEntries] or [onData].
+ const FullEntryBuilder({
+ super.key,
+ this.onEntries,
+ this.onData,
+ required this.rangeType,
+ }) : assert((onEntries == null) != (onData == null), 'Provide either of the builders.');
+
+ /// Builder using a sorted list of full entries.
+ final Widget Function(BuildContext context, List<FullEntry> entries)? onEntries;
+
+ /// Builder using data from the main categories.
+ final Widget Function(BuildContext context, List<BloodPressureRecord> records, List<MedicineIntake> intakes, List<Note> notes)? onData;
+
+ /// Range type to load entries from.
+ final IntervalStoreManagerLocation rangeType;
+
+ @override
+ Widget build(BuildContext context) => RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
+ rangeType: rangeType,
+ onData: (context, records) => RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
+ rangeType: rangeType,
+ onData: (BuildContext context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
+ rangeType: rangeType,
+ onData: (BuildContext context, List<Note> notes) {
+ if (onData != null) return onData!(context, records, intakes, notes);
+
+ final entries = FullEntryList.merged(records, notes, intakes);
+ entries.sort((a, b) => b.time.compareTo(a.time)); // newest first
+ return onEntries!(context, entries);
+ },
+ ),
+ ),
+ );
+}
app/lib/features/measurement_list/weight_list.dart
@@ -0,0 +1,42 @@
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+
+/// List of weights recorded in the contexts [BodyweightRepository].
+class WeightList extends StatelessWidget {
+ /// Create a list of weights
+ const WeightList({super.key, required this.rangeType});
+
+ /// The location from which the displayed interval is taken.
+ final IntervalStoreManagerLocation rangeType;
+
+ @override
+ Widget build(BuildContext context) {
+ final format = DateFormat(context.select<Settings, String>((s) => s.dateFormatString));
+ return RepositoryBuilder<BodyweightRecord, BodyweightRepository>(
+ rangeType: rangeType,
+ onData: (context, records) {
+ records.sort((a, b) => b.time.compareTo(a.time));
+ return ListView.builder(
+ itemCount: records.length,
+ itemBuilder: (context, idx) => ListTile(
+ title: Text(_buildWeightText(records[idx].weight)),
+ subtitle: Text(format.format(records[idx].time))
+ ),
+ );
+ },
+ );
+ }
+
+ String _buildWeightText(Weight w) {
+ String weightStr = w.kg.toStringAsFixed(2);
+ if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
+ if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
+ if (weightStr.endsWith('.')) weightStr = weightStr.substring(0, weightStr.length - 1);
+ // TODO: preferred weight unit
+ return '$weightStr kg';
+ }
+}
app/lib/l10n/app_en.arb
@@ -516,5 +516,7 @@
"invalidZip": "Invalid zip file.",
"@invalidZip": {},
"errCantCreateArchive": "Can''t create archive. Please report the bug if possible.",
- "@errCantCreateArchive": {}
+ "@errCantCreateArchive": {},
+ "activateWeightFeatures": "Activate weight related features",
+ "@weightInput": {}
}
\ No newline at end of file
app/lib/model/storage/settings_store.dart
@@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
List<String>? knownBleDev,
int? highestMedIndex,
bool? bleInput,
+ bool? weightInput,
}) {
if (accentColor != null) _accentColor = accentColor;
if (sysColor != null) _sysColor = sysColor;
@@ -76,6 +77,7 @@ class Settings extends ChangeNotifier {
if (highestMedIndex != null) _highestMedIndex = highestMedIndex;
if (knownBleDev != null) _knownBleDev = knownBleDev;
if (bleInput != null) _bleInput = bleInput;
+ if (weightInput != null) _weightInput = weightInput;
_language = language; // No check here, as null is the default as well.
}
@@ -110,6 +112,7 @@ class Settings extends ChangeNotifier {
highestMedIndex: ConvertUtil.parseInt(map['highestMedIndex']),
knownBleDev: ConvertUtil.parseList<String>(map['knownBleDev']),
bleInput: ConvertUtil.parseBool(map['bleInput']),
+ weightInput: ConvertUtil.parseBool(map['weightInput']),
);
// update
@@ -157,6 +160,7 @@ class Settings extends ChangeNotifier {
'preferredPressureUnit': preferredPressureUnit.encode(),
'knownBleDev': knownBleDev,
'bleInput': bleInput,
+ 'weightInput': weightInput,
};
/// Serialize the object to a restoreable string.
@@ -192,6 +196,7 @@ class Settings extends ChangeNotifier {
_medications.clear();
_medications.addAll(other._medications);
_highestMedIndex = other._highestMedIndex;
+ _weightInput = other._weightInput;
notifyListeners();
}
@@ -408,6 +413,14 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
+ bool _weightInput = false;
+ /// Whether to show weight related features.
+ bool get weightInput => _weightInput;
+ set weightInput(bool value) {
+ _weightInput = value;
+ notifyListeners();
+ }
+
List<String> _knownBleDev = [];
/// Bluetooth devices that previously connected.
///
app/lib/screens/home_screen.dart
@@ -1,25 +1,19 @@
-import 'dart:collection';
-
-import 'package:blood_pressure_app/data_util/blood_pressure_builder.dart';
import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/data_util/full_entry_builder.dart';
import 'package:blood_pressure_app/data_util/interval_picker.dart';
-import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';
+import 'package:blood_pressure_app/features/measurement_list/weight_list.dart';
import 'package:blood_pressure_app/features/statistics/value_graph.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/screens/settings_screen.dart';
-import 'package:blood_pressure_app/screens/statistics_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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';
-/// Is true during the first [AppHome.build] before creating the widget.
-bool _appStart = true;
+/// 0 when add entry dialoge has not been shown, 1 when dialoge is scheduled, 2 when dialoge was launched.
+int _appStart = 0;
/// Central screen of the app with graph and measurement list that is the center
/// of navigation.
@@ -28,146 +22,82 @@ class AppHome extends StatelessWidget {
const AppHome({super.key});
Widget _buildValueGraph(BuildContext context) => Padding(
- padding: const EdgeInsets.only(right: 8, left: 2, top: 16),
- child: Column(
- children: [
- SizedBox(
- height: 240,
- width: MediaQuery.of(context).size.width,
- // TODO: stop duplicating this complex construct
- child: RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (context, List<Note> notes) => BloodPressureBuilder(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (BuildContext context, UnmodifiableListView<BloodPressureRecord> records) => BloodPressureValueGraph(
- records: records,
- colors: notes,
- intakes: intakes,
- ),
- ),
- ),
- ),
+ padding: const EdgeInsets.only(right: 8, top: 16),
+ child: SizedBox(
+ height: 240.0,
+ width: MediaQuery.of(context).size.width,
+ child: FullEntryBuilder(
+ rangeType: IntervalStoreManagerLocation.mainPage,
+ onData: (context, records, intakes, notes) => BloodPressureValueGraph(
+ records: records,
+ colors: notes,
+ intakes: intakes,
),
- const IntervalPicker(type: IntervalStoreManagerLocation.mainPage),
- ],
+ ),
+ ),
+ );
+
+ Widget _buildMeasurementList(BuildContext context) => FullEntryBuilder(
+ rangeType: IntervalStoreManagerLocation.mainPage,
+ onEntries: (context, entries) => Padding(
+ padding: const EdgeInsets.only(top: 12.0),
+ child: (context.select<Settings, bool>((s) => s.compactList))
+ ? CompactMeasurementList(data: entries)
+ : MeasurementList(entries: entries),
),
);
@override
- Widget build(BuildContext context) {
- final localizations = AppLocalizations.of(context)!;
- // direct use of settings possible as no listening is required
- if (_appStart) {
- if (Provider.of<Settings>(context, listen: false).startWithAddMeasurementPage) {
- SchedulerBinding.instance.addPostFrameCallback((_) => context.createEntry());
+ Widget build(BuildContext context) => OrientationBuilder(
+ builder: (BuildContext context, Orientation orientation) {
+ // direct use of settings possible as no listening is required
+ if (_appStart == 0) {
+ if (Provider.of<Settings>(context, listen: false).startWithAddMeasurementPage) {
+ SchedulerBinding.instance.addPostFrameCallback((_) {
+ if (context.mounted) {
+ context.createEntry();
+ _appStart++;
+ } else {
+ _appStart--;
+ }
+
+ });
+ }
+ _appStart++;
}
- }
- _appStart = false;
- return Scaffold(
- body: OrientationBuilder(
- builder: (context, orientation) {
- if (orientation == Orientation.landscape) return _buildValueGraph(context);
- return Center(
- child: Padding(
- padding: const EdgeInsets.only(top: 20),
- child: Consumer<IntervalStoreManager>(builder: (context, intervalls, child) =>
- Column(children: [
- _buildValueGraph(context),
- Expanded(
- child: BloodPressureBuilder(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (context, records) => RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (BuildContext context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
- rangeType: IntervalStoreManagerLocation.mainPage,
- onData: (BuildContext context, List<Note> notes) {
- final entries = FullEntryList.merged(records, notes, intakes);
- entries.sort((a, b) => b.time.compareTo(a.time)); // newest first
- return (context.select<Settings, bool>((s) => s.compactList))
- ? CompactMeasurementList(data: entries)
- : MeasurementList(entries: entries);
- },
- ),
- ),
- ),
- ),
- ],),),
- ),
- );
- },
- ),
- floatingActionButton: OrientationBuilder(
- builder: (context, orientation) {
- if (orientation == Orientation.landscape && MediaQuery.of(context).size.height < 500) {
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
- return const SizedBox.shrink();
- }
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
- return Consumer<Settings>(builder: (context, settings, child) => Column(
- verticalDirection: VerticalDirection.up,
- children: [
- SizedBox.square(
- dimension: 75,
- child: FittedBox(
- child: FloatingActionButton(
- heroTag: 'floatingActionAdd',
- tooltip: localizations.addMeasurement,
- autofocus: true,
- onPressed: context.createEntry,
- child: const Icon(Icons.add,),
+ if (orientation == Orientation.landscape) return _buildValueGraph(context);
+ return DefaultTabController(
+ length: 2,
+ child: Scaffold(
+ body: CustomScrollView(
+ slivers: [
+ SliverToBoxAdapter(child: _buildValueGraph(context),),
+ const SliverToBoxAdapter(child: IntervalPicker(type: IntervalStoreManagerLocation.mainPage)),
+ if (!(context.select<Settings, bool>((s) => s.weightInput)))
+ SliverFillRemaining(child: _buildMeasurementList(context)),
+
+ if ((context.select<Settings, bool>((s) => s.weightInput)))
+ const SliverToBoxAdapter(child: TabBar(
+ tabs: [
+ Tab(icon: Icon(Icons.monitor_heart)),
+ Tab(icon: Icon(Icons.scale)),
+ ],
+ )),
+ if ((context.select<Settings, bool>((s) => s.weightInput)))
+ SliverFillRemaining(
+ child: TabBarView(
+ children: [
+ _buildMeasurementList(context),
+ const WeightList(rangeType: IntervalStoreManagerLocation.mainPage),
+ ]
),
- ),
- ),
- const SizedBox(
- height: 10,
- ),
- FloatingActionButton(
- heroTag: 'floatingActionStatistics',
- tooltip: localizations.statistics,
- backgroundColor: const Color(0xFF6F6F6F),
- onPressed: () {
- _buildTransition(context, const StatisticsScreen(), settings.animationSpeed);
- },
- child: const Icon(Icons.insights, color: Colors.black),
- ),
- const SizedBox(
- height: 10,
- ),
- FloatingActionButton(
- heroTag: 'floatingActionSettings',
- tooltip: localizations.settings,
- backgroundColor: const Color(0xFF6F6F6F),
- child: const Icon(Icons.settings, color: Colors.black),
- onPressed: () {
- _buildTransition(context, const SettingsPage(), settings.animationSpeed);
- },
- ),
+ )
],
- ),);
- },
- ),
- );
- }
-}
-
-// TODO: consider removing duration override that only occurs in one on home.
-void _buildTransition(BuildContext context, Widget page, int duration) {
- Navigator.push(context,
- _TimedMaterialPageRouter(
- transitionDuration: Duration(milliseconds: duration),
- builder: (context) => page,
- ),
+ ),
+ floatingActionButton: const NavigationActionButtons(),
+ ),
+ );
+ },
);
}
-
-class _TimedMaterialPageRouter extends MaterialPageRoute {
- _TimedMaterialPageRouter({
- required super.builder,
- required this.transitionDuration,});
-
- @override
- final Duration transitionDuration;
-}
\ No newline at end of file
app/lib/screens/settings_screen.dart
@@ -292,6 +292,13 @@ class SettingsPage extends StatelessWidget {
if (value != null) settings.preferredPressureUnit = value;
},
),
+ SwitchListTile(
+ value: settings.weightInput,
+ title: Text(localizations.activateWeightFeatures),
+ secondary: const Icon(Icons.scale),
+ onChanged: (value) {
+ settings.weightInput = value;
+ },),
],),
TitledColumn(
title: Text(localizations.data),
app/lib/screens/statistics_screen.dart
@@ -1,11 +1,12 @@
-import 'package:blood_pressure_app/data_util/blood_pressure_builder.dart';
import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
import 'package:blood_pressure_app/features/statistics/blood_pressure_distribution.dart';
import 'package:blood_pressure_app/features/statistics/clock_bp_graph.dart';
import 'package:blood_pressure_app/model/blood_pressure_analyzer.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
/// A page that shows statistics about stored blood pressure values.
class StatisticsScreen extends StatefulWidget {
@@ -24,7 +25,7 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
appBar: AppBar(
title: Text(localizations.statistics),
),
- body: BloodPressureBuilder(
+ body: RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
rangeType: IntervalStoreManagerLocation.statsPage,
onData: (context, data) {
final analyzer = BloodPressureAnalyser(data.toList());
app/lib/app.dart
@@ -99,6 +99,7 @@ class _AppState extends State<App> {
late NoteRepository noteRepo;
late MedicineRepository medRepo;
late MedicineIntakeRepository intakeRepo;
+ late BodyweightRepository weightRepo;
try {
_entryDB = await openDatabase(
@@ -109,6 +110,7 @@ class _AppState extends State<App> {
noteRepo = db.noteRepo;
medRepo = db.medRepo;
intakeRepo = db.intakeRepo;
+ weightRepo = db.weightRepo;
} catch (e, stack) {
await ErrorReporting.reportCriticalError('Error loading entry db', '$e\n$stack',);
}
@@ -180,6 +182,7 @@ class _AppState extends State<App> {
RepositoryProvider.value(value: noteRepo),
RepositoryProvider.value(value: medRepo),
RepositoryProvider.value(value: intakeRepo),
+ RepositoryProvider.value(value: weightRepo),
],
child: MultiProvider(
providers: [
app/test/data_util/entry_context_test.dart
@@ -14,9 +14,9 @@ void main() {
testWidgets('fully deletes entries', (tester) async {
final entry = mockEntry(time: DateTime.now(), sys: 123, note: 'test', intake: mockIntake(mockMedicine()));
- final BloodPressureRepository bpRepo = _MockBp() as BloodPressureRepository;
- final NoteRepository noteRepo = _MockNote() as NoteRepository;
- final MedicineIntakeRepository intakeRepo = _MockIntake() as MedicineIntakeRepository;
+ final BloodPressureRepository bpRepo = MockBloodPressureRepository() as BloodPressureRepository;
+ final NoteRepository noteRepo = MockNoteRepository() as NoteRepository;
+ final MedicineIntakeRepository intakeRepo = MockMedicineIntakeRepository() as MedicineIntakeRepository;
await bpRepo.add(entry.$1);
await noteRepo.add(entry.$2);
await intakeRepo.add(entry.$3.first);
@@ -50,32 +50,3 @@ void main() {
expect(await intakeRepo.get(DateRange.all()), isEmpty);
});
}
-
-class _MockRepo<T> extends Repository<T> {
- List<T> data = [];
-
- @override
- Future<void> add(T value) async => data.add(value);
-
- @override
- Future<List<T>> get(DateRange range) async => data;
-
- @override
- Future<void> remove(T value) async => data.remove(value);
-
- @override
- Stream subscribe() => Stream.empty();
-}
-
-class _MockBp extends _MockRepo<BloodPressureRecord> implements BloodPressureRepository {
- @override
- dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
-class _MockNote extends _MockRepo<Note> implements NoteRepository {
- @override
- dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
-class _MockIntake extends _MockRepo<MedicineIntake> implements MedicineIntakeRepository {
- @override
- dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
app/test/data_util/full_entry_builder_test.dart
@@ -0,0 +1,58 @@
+import 'package:blood_pressure_app/data_util/full_entry_builder.dart';
+import 'package:blood_pressure_app/model/storage/interval_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 '../features/measurement_list/measurement_list_entry_test.dart';
+import '../model/analyzer_test.dart';
+import '../util.dart';
+
+void main() {
+ testWidgets('loads expected data', (tester) async {
+ final records = [mockRecord(sys: 123)];
+ final notes = [Note(time: DateTime(2000), note: 'test1'), Note(time: DateTime(2001), note: 'test2')];
+ final intakes = [mockIntake(mockMedicine()), mockIntake(mockMedicine()), mockIntake(mockMedicine())];
+
+ final mainIntervalls = IntervalStorage();
+ mainIntervalls.changeStepSize(TimeStep.lifetime);
+
+ await tester.pumpWidget(await appBaseWithData(FullEntryBuilder(
+ rangeType: IntervalStoreManagerLocation.mainPage,
+ onData: (context, foundRecords, foundIntakes, foundNotes) {
+ expect(foundRecords, records);
+ expect(foundIntakes, intakes);
+ expect(foundNotes, notes);
+ return const Text('ok');
+ },
+ ), records: records, intakes: intakes, notes: notes, intervallStoreManager: IntervalStoreManager(mainIntervalls, IntervalStorage(), IntervalStorage())));
+
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ expect(find.text(localizations.loading), findsOneWidget);
+ await tester.pumpAndSettle();
+ expect(find.text('ok'), findsOneWidget);
+ });
+ testWidgets('loads sorted entries', (tester) async {
+ final notes = [Note(time: DateTime(2003), note: 'test0'), Note(time: DateTime(2000), note: 'test1'), Note(time: DateTime(2001), note: 'test2')];
+
+ final exportPageIntervalls = IntervalStorage();
+ exportPageIntervalls.changeStepSize(TimeStep.lifetime);
+
+ await tester.pumpWidget(await appBaseWithData(FullEntryBuilder(
+ rangeType: IntervalStoreManagerLocation.exportPage,
+ onEntries: (context, entries) {
+ expect(entries, hasLength(3));
+ expect(entries[0].time.year, 2003);
+ expect(entries[1].time.year, 2001);
+ expect(entries[2].time.year, 2000);
+ return const Text('ok');
+ },
+ ), notes: notes, intervallStoreManager: IntervalStoreManager(exportPageIntervalls, IntervalStorage(), IntervalStorage())));
+
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ expect(find.text(localizations.loading), findsOneWidget);
+ await tester.pumpAndSettle();
+ expect(find.text('ok'), findsOneWidget);
+ });
+}
app/test/features/measurement_list/weight_list_test.dart
@@ -0,0 +1,45 @@
+import 'package:blood_pressure_app/features/measurement_list/weight_list.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+import '../../util.dart';
+
+void main() {
+ testWidgets('shows all elements in time range in order', (tester) async {
+ final interval = IntervalStorage();
+ interval.changeStepSize(TimeStep.lifetime);
+
+ await tester.pumpWidget(await appBaseWithData(
+ weights: [
+ BodyweightRecord(time: DateTime(2001), weight: Weight.kg(123.0)),
+ BodyweightRecord(time: DateTime(2003), weight: Weight.kg(122.1)),
+ BodyweightRecord(time: DateTime(2000), weight: Weight.kg(70.0)),
+ BodyweightRecord(time: DateTime(2002), weight: Weight.kg(7000.12345)),
+ ],
+ intervallStoreManager: IntervalStoreManager(interval, IntervalStorage(), IntervalStorage()),
+ const WeightList(rangeType: IntervalStoreManagerLocation.mainPage),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(ListTile), findsNWidgets(4));
+ expect(find.text('123 kg'), findsOneWidget);
+ expect(find.text('122.1 kg'), findsOneWidget);
+ expect(find.text('70 kg'), findsOneWidget);
+ expect(find.text('7000.12 kg'), findsOneWidget);
+
+ expect(
+ tester.getCenter(find.textContaining('2003')).dy,
+ lessThan(tester.getCenter(find.textContaining('2002')).dy),
+ );
+ expect(
+ tester.getCenter(find.textContaining('2002')).dy,
+ lessThan(tester.getCenter(find.textContaining('2001')).dy),
+ );
+ expect(
+ tester.getCenter(find.textContaining('2001')).dy,
+ lessThan(tester.getCenter(find.textContaining('2000')).dy),
+ );
+ });
+}
app/test/model/json_serialization_test.dart
@@ -96,6 +96,7 @@ void main() {
bottomAppBars: true,
knownBleDev: ['a', 'b'],
bleInput: false,
+ weightInput: true,
);
final fromJson = Settings.fromJson(initial.toJson());
@@ -124,6 +125,7 @@ void main() {
expect(initial.bottomAppBars, fromJson.bottomAppBars);
expect(initial.knownBleDev, fromJson.knownBleDev);
expect(initial.bleInput, fromJson.bleInput);
+ expect(initial.weightInput, fromJson.weightInput);
expect(initial.toJson(), fromJson.toJson());
});
app/test/screens/home_screen_test.dart
@@ -0,0 +1,70 @@
+import 'dart:ui';
+
+import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
+import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
+import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';
+import 'package:blood_pressure_app/features/statistics/value_graph.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../model/analyzer_test.dart';
+import '../util.dart';
+
+void main() {
+ final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
+
+ testWidgets('shows graph above list in phone mode', (tester) async {
+ await binding.setSurfaceSize(const Size(400, 800));
+
+ await tester.pumpWidget(await appBaseWithData(const AppHome()));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(NavigationActionButtons), findsOneWidget);
+
+ expect(find.byType(BloodPressureValueGraph), findsOneWidget);
+ expect(find.byType(IntervalPicker), findsOneWidget);
+ expect(find.byType(MeasurementList), findsOneWidget);
+
+ expect(
+ tester.getCenter(find.byType(BloodPressureValueGraph)).dy,
+ lessThan(tester.getCenter(find.byType(IntervalPicker)).dy)
+ );
+ expect(
+ tester.getCenter(find.byType(IntervalPicker)).dy,
+ lessThan(tester.getCenter(find.byType(MeasurementList)).dy)
+ );
+ });
+
+ testWidgets('only shows graph in landscape more', (tester) async {
+ await binding.setSurfaceSize(const Size(800, 400));
+
+ await tester.pumpWidget(await appBaseWithData(const AppHome(),
+ records: [mockRecord(sys: 123)],
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(BloodPressureValueGraph), findsOneWidget);
+ expect(find.byType(NavigationActionButtons), findsNothing);
+ expect(find.byType(IntervalPicker), findsNothing);
+ expect(find.byType(MeasurementList), findsNothing);
+ });
+
+ testWidgets('respects compact list setting', (tester) async {
+ await binding.setSurfaceSize(const Size(400, 800));
+
+ final s = Settings(useLegacyList: false);
+ await tester.pumpWidget(await appBaseWithData(const AppHome(), settings: s));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(MeasurementList), findsOneWidget);
+ expect(find.byType(CompactMeasurementList), findsNothing);
+
+ s.compactList = true;
+ await tester.pump();
+
+ expect(find.byType(MeasurementList), findsNothing);
+ expect(find.byType(CompactMeasurementList), findsOneWidget);
+ });
+}
app/test/util.dart
@@ -7,7 +7,6 @@ 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:provider/provider.dart';
-import 'package:sqflite_common_ffi/sqflite_ffi.dart';
/// Create a root material widget with localizations.
Widget materialApp(Widget child, {
@@ -39,7 +38,7 @@ Widget materialApp(Widget child, {
}
/// Creates a the same App as the main method.
-Future<Widget> appBase(Widget child, {
+Widget appBase(Widget child, {
Settings? settings,
ExportSettings? exportSettings,
CsvExportSettings? csvExportSettings,
@@ -47,11 +46,18 @@ Future<Widget> appBase(Widget child, {
IntervalStoreManager? intervallStoreManager,
BloodPressureRepository? bpRepo,
MedicineRepository? medRepo,
+ NoteRepository? noteRepo,
MedicineIntakeRepository? intakeRepo,
-}) async {
+ BodyweightRepository? weightRepo,
+}) {
HealthDataStore? db;
- if (bpRepo == null || medRepo == null || intakeRepo == null) {
- db = await _getHealthDateStore();
+ if (bpRepo == null
+ || medRepo == null
+ || intakeRepo == null
+ || noteRepo == null
+ || weightRepo == null
+ ) {
+ db = MockHealthDataSore();
}
return MultiRepositoryProvider(
@@ -59,6 +65,8 @@ Future<Widget> appBase(Widget child, {
RepositoryProvider(create: (context) => bpRepo ?? db!.bpRepo),
RepositoryProvider(create: (context) => medRepo ?? db!.medRepo),
RepositoryProvider(create: (context) => intakeRepo ?? db!.intakeRepo),
+ RepositoryProvider(create: (context) => noteRepo ?? db!.noteRepo),
+ RepositoryProvider(create: (context) => weightRepo ?? db!.weightRepo),
],
child: materialApp(child,
settings: settings,
@@ -79,15 +87,31 @@ Future<Widget> appBaseWithData(Widget child, {
IntervalStoreManager? intervallStoreManager,
List<BloodPressureRecord>? records,
List<Medicine>? meds,
+ List<Note>? notes,
List<MedicineIntake>? intakes,
+ List<BodyweightRecord>? weights,
}) async {
- final db = await _getHealthDateStore();
+ final db = MockHealthDataSore();
final bpRepo = db.bpRepo;
for (final r in records ?? []) {
await bpRepo.add(r);
}
final medRepo = db.medRepo;
+ for (final m in meds ?? []) {
+ await medRepo.add(m);
+ }
final intakeRepo = db.intakeRepo;
+ for (final i in intakes ?? []) {
+ await intakeRepo.add(i);
+ }
+ final noteRepo = db.noteRepo;
+ for (final n in notes ?? []) {
+ await noteRepo.add(n);
+ }
+ final weightRepo = db.weightRepo;
+ for (final w in weights ?? []) {
+ await weightRepo.add(w);
+ }
return appBase(
child,
@@ -98,7 +122,9 @@ Future<Widget> appBaseWithData(Widget child, {
intervallStoreManager: intervallStoreManager,
bpRepo: bpRepo,
medRepo: medRepo,
+ noteRepo: noteRepo,
intakeRepo: intakeRepo,
+ weightRepo: weightRepo,
);
}
@@ -185,12 +211,44 @@ Medicine mockMedicine({
return med;
}
-/// Don't use this, use [_getHealthDateStore] to obtain.
-HealthDataStore? _db;
-Future<HealthDataStore> _getHealthDateStore() async {
- TestWidgetsFlutterBinding.ensureInitialized();
- sqfliteFfiInit();
- final db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
- _db ??= await HealthDataStore.load(db);
- return _db!;
+class MockHealthDataSore implements HealthDataStore {
+ @override
+ BloodPressureRepository bpRepo = MockBloodPressureRepository();
+
+ @override
+ MedicineIntakeRepository intakeRepo = MockMedicineIntakeRepository();
+
+ @override
+ MedicineRepository medRepo = MockMedicineRepository();
+
+ @override
+ NoteRepository noteRepo = MockNoteRepository();
+
+ @override
+ BodyweightRepository weightRepo = MockBodyweightRepository();
+}
+
+class _MockRepo<T> extends Repository<T> {
+ List<T> data = [];
+
+ @override
+ Future<void> add(T value) async => data.add(value);
+
+ @override
+ Future<List<T>> get(DateRange range) async => data;
+
+ @override
+ Future<void> remove(T value) async => data.remove(value);
+
+ @override
+ Stream subscribe() => const Stream.empty(); // FIXME
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => throw Exception('unexpected call: $invocation');
}
+
+class MockBloodPressureRepository extends _MockRepo<BloodPressureRecord> implements BloodPressureRepository {}
+class MockMedicineIntakeRepository extends _MockRepo<MedicineIntake> implements MedicineIntakeRepository {}
+class MockMedicineRepository extends _MockRepo<Medicine> implements MedicineRepository {}
+class MockNoteRepository extends _MockRepo<Note> implements NoteRepository {}
+class MockBodyweightRepository extends _MockRepo<BodyweightRecord> implements BodyweightRepository {}
app/pubspec.lock
@@ -500,18 +500,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
- version: "10.0.7"
+ version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
- version: "3.0.8"
+ version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
@@ -852,7 +852,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
- version: "0.0.0"
+ version: "0.0.99"
source_gen:
dependency: transitive
description:
@@ -1097,10 +1097,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
url: "https://pub.dev"
source: hosted
- version: "14.2.5"
+ version: "14.2.4"
watcher:
dependency: transitive
description: