diff --git a/lib/helpers/ui.dart b/lib/helpers/ui.dart index d96b4b5b..2de98a5d 100644 --- a/lib/helpers/ui.dart +++ b/lib/helpers/ui.dart @@ -101,7 +101,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, BuildContext cont // This call serves no purpose The dialog above doesn't seem to show // unless this dummy call is present - // showDialog(context: context, builder: (context) => Container()); + showDialog(context: context, builder: (context) => Container()); } dynamic showDeleteDialog( diff --git a/lib/models/body_weight/weight_entry.dart b/lib/models/body_weight/weight_entry.dart index 1fbb5dbf..cb0dc944 100644 --- a/lib/models/body_weight/weight_entry.dart +++ b/lib/models/body_weight/weight_entry.dart @@ -27,7 +27,7 @@ class WeightEntry { int? id; @JsonKey(required: true, fromJson: stringToNum, toJson: numToString) - late num weight; + late num weight = 0; @JsonKey(required: true, toJson: toDate) late DateTime date; @@ -44,7 +44,14 @@ class WeightEntry { } } + WeightEntry copyWith({int? id, int? weight, DateTime? date}) => WeightEntry( + id: id, + weight: weight ?? this.weight, + date: date ?? this.date, + ); + // Boilerplate factory WeightEntry.fromJson(Map json) => _$WeightEntryFromJson(json); + Map toJson() => _$WeightEntryToJson(this); } diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart index 35b10ff2..4ecba70b 100644 --- a/lib/providers/body_weight.dart +++ b/lib/providers/body_weight.dart @@ -43,9 +43,9 @@ class BodyWeightProvider with ChangeNotifier { _entries = []; } - /// Returns the latest (newest) weight entry or null if there are no entries - WeightEntry? getLastEntry() { - return _entries.isNotEmpty ? _entries.last : null; + /// Returns the latest (newest) weight entry or null if there are none + WeightEntry? getNewestEntry() { + return _entries.isNotEmpty ? _entries.first : null; } WeightEntry findById(int id) { diff --git a/lib/screens/weight_screen.dart b/lib/screens/weight_screen.dart index d3040c93..c133e816 100644 --- a/lib/screens/weight_screen.dart +++ b/lib/screens/weight_screen.dart @@ -30,6 +30,8 @@ class WeightScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final lastWeightEntry = context.read().getNewestEntry(); + return Scaffold( appBar: EmptyAppBar(AppLocalizations.of(context).weight), floatingActionButton: FloatingActionButton( @@ -43,7 +45,7 @@ class WeightScreen extends StatelessWidget { FormScreen.routeName, arguments: FormScreenArguments( AppLocalizations.of(context).newEntry, - WeightForm(), + WeightForm(lastWeightEntry?.copyWith(id: null)), ), ); }, diff --git a/lib/widgets/nutrition/nutritional_plan_detail.dart b/lib/widgets/nutrition/nutritional_plan_detail.dart index f09163eb..ac6611ac 100644 --- a/lib/widgets/nutrition/nutritional_plan_detail.dart +++ b/lib/widgets/nutrition/nutritional_plan_detail.dart @@ -42,7 +42,8 @@ class NutritionalPlanDetailWidget extends StatelessWidget { Widget build(BuildContext context) { final nutritionalValues = _nutritionalPlan.nutritionalValues; final valuesPercentage = _nutritionalPlan.energyPercentage(nutritionalValues); - final lastWeightEntry = Provider.of(context, listen: false).getLastEntry(); + final lastWeightEntry = + Provider.of(context, listen: false).getNewestEntry(); final valuesGperKg = lastWeightEntry != null ? _nutritionalPlan.gPerBodyKg(lastWeightEntry.weight, nutritionalValues) : null; diff --git a/lib/widgets/weight/forms.dart b/lib/widgets/weight/forms.dart index 1f414b23..1f47f051 100644 --- a/lib/widgets/weight/forms.dart +++ b/lib/widgets/weight/forms.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/json.dart'; @@ -34,7 +35,7 @@ class WeightForm extends StatelessWidget { WeightForm([WeightEntry? weightEntry]) { _weightEntry = weightEntry ?? WeightEntry(date: DateTime.now()); - weightController.text = _weightEntry.id == null ? '' : _weightEntry.weight.toString(); + weightController.text = _weightEntry.weight == 0 ? '' : _weightEntry.weight.toString(); dateController.text = toDate(_weightEntry.date)!; } @@ -46,10 +47,15 @@ class WeightForm extends StatelessWidget { children: [ // Weight date TextFormField( - readOnly: true, // Stop keyboard from appearing + key: const Key('dateInput'), + readOnly: true, + // Stop keyboard from appearing decoration: InputDecoration( labelText: AppLocalizations.of(context).date, - suffixIcon: const Icon(Icons.calendar_today), + suffixIcon: const Icon( + Icons.calendar_today, + key: Key('calendarIcon'), + ), ), enableInteractiveSelection: false, controller: dateController, @@ -83,7 +89,60 @@ class WeightForm extends StatelessWidget { // Weight TextFormField( - decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), + key: const Key('weightInput'), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).weight, + prefix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + key: const Key('quickMinus'), + icon: const FaIcon(FontAwesomeIcons.circleMinus), + onPressed: () { + try { + final num newValue = num.parse(weightController.text) - 1; + weightController.text = newValue.toString(); + } on FormatException {} + }, + ), + IconButton( + key: const Key('quickMinusSmall'), + icon: const FaIcon(FontAwesomeIcons.minus), + onPressed: () { + try { + final num newValue = num.parse(weightController.text) - 0.25; + weightController.text = newValue.toString(); + } on FormatException {} + }, + ), + ], + ), + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + key: const Key('quickPlusSmall'), + icon: const FaIcon(FontAwesomeIcons.plus), + onPressed: () { + try { + final num newValue = num.parse(weightController.text) + 0.25; + weightController.text = newValue.toString(); + } on FormatException {} + }, + ), + IconButton( + key: const Key('quickPlus'), + icon: const FaIcon(FontAwesomeIcons.circlePlus), + onPressed: () { + try { + final num newValue = num.parse(weightController.text) + 1; + weightController.text = newValue.toString(); + } on FormatException {} + }, + ), + ], + ), + ), controller: weightController, keyboardType: TextInputType.number, onSaved: (newValue) { @@ -113,11 +172,10 @@ class WeightForm extends StatelessWidget { // Save the entry on the server try { + final provider = Provider.of(context, listen: false); _weightEntry.id == null - ? await Provider.of(context, listen: false) - .addEntry(_weightEntry) - : await Provider.of(context, listen: false) - .editEntry(_weightEntry); + ? await provider.addEntry(_weightEntry) + : await provider.editEntry(_weightEntry); } on WgerHttpException catch (error) { if (context.mounted) { showHttpExceptionErrorDialog(error, context); diff --git a/pubspec.lock b/pubspec.lock index caf63741..c92bb01f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -341,10 +341,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_selector_linux: dependency: transitive description: @@ -764,6 +764,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -808,26 +832,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -896,10 +920,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -968,10 +992,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1032,10 +1056,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" provider: dependency: "direct main" description: @@ -1461,10 +1485,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -1493,10 +1517,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" win32: dependency: transitive description: diff --git a/test/weight/weight_form_test.dart b/test/weight/weight_form_test.dart new file mode 100644 index 00000000..e1158da3 --- /dev/null +++ b/test/weight/weight_form_test.dart @@ -0,0 +1,86 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (C) 2020, 2021 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * wger Workout Manager is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:wger/models/body_weight/weight_entry.dart'; +import 'package:wger/widgets/weight/forms.dart'; + +import '../../test_data/body_weight.dart'; + +void main() { + Widget createWeightForm({locale = 'en', weightEntry = WeightEntry}) { + return MaterialApp( + locale: Locale(locale), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: WeightForm(weightEntry), + ), + ); + } + + testWidgets('The form is prefilled with the data from an entry', (WidgetTester tester) async { + await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1)); + await tester.pumpAndSettle(); + + expect(find.text('2021-01-01'), findsOneWidget); + expect(find.text('80'), findsOneWidget); + }); + + testWidgets('It is possible to quick-change the weight', (WidgetTester tester) async { + await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('quickMinus'))); + expect(find.text('79'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickMinusSmall'))); + expect(find.text('78.75'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickPlus'))); + expect(find.text('79.75'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickPlusSmall'))); + expect(find.text('80.0'), findsOneWidget); + }); + + testWidgets("Entering garbage doesn't break the quick-change", (WidgetTester tester) async { + await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1)); + await tester.pumpAndSettle(); + await tester.enterText(find.byKey(const Key('weightInput')), 'shiba inu'); + + await tester.tap(find.byKey(const Key('quickMinus'))); + expect(find.text('shiba inu'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickMinusSmall'))); + expect(find.text('shiba inu'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickPlus'))); + expect(find.text('shiba inu'), findsOneWidget); + + await tester.tap(find.byKey(const Key('quickPlusSmall'))); + expect(find.text('shiba inu'), findsOneWidget); + }); + + testWidgets('Widget works if there is no last entry', (WidgetTester tester) async { + await tester.pumpWidget(createWeightForm(weightEntry: null)); + await tester.pumpAndSettle(); + }); +} diff --git a/test/weight/weight_screen_test.dart b/test/weight/weight_screen_test.dart index 78138d3f..27a062bb 100644 --- a/test/weight/weight_screen_test.dart +++ b/test/weight/weight_screen_test.dart @@ -41,6 +41,7 @@ void main() { setUp(() { mockWeightProvider = MockBodyWeightProvider(); when(mockWeightProvider.items).thenReturn(getWeightEntries()); + when(mockWeightProvider.getNewestEntry()).thenReturn(null); mockUserProvider = MockUserProvider(); when(mockUserProvider.profile).thenReturn(tProfile1); diff --git a/test_data/body_weight.dart b/test_data/body_weight.dart index 8cec705c..81dc4e0d 100644 --- a/test_data/body_weight.dart +++ b/test_data/body_weight.dart @@ -18,9 +18,9 @@ import 'package:wger/models/body_weight/weight_entry.dart'; -final weightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01)); -final weightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10)); +final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01)); +final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10)); List getWeightEntries() { - return [weightEntry1, weightEntry2]; + return [testWeightEntry1, testWeightEntry2]; }