diff --git a/.gitignore b/.gitignore index 728c5893..0df000b8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ .pub-cache/ .pub/ /build/ +**/failures/*.png + # Web related lib/generated_plugin_registrant.dart diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index 05805408..a90b83c0 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -64,7 +64,6 @@ class Meal { this.mealItems = mealItems ?? []; this.diaryEntries = diaryEntries ?? []; - time = time ?? TimeOfDay.fromDateTime(DateTime.now()); this.name = name ?? ''; } diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index ba158efe..afd50d81 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -212,7 +212,6 @@ class NutritionalPlan { id: PSEUDO_MEAL_ID, plan: id, name: name, - time: null, diaryEntries: diaryEntries.where((e) => e.mealId == null).toList(), ); } diff --git a/lib/widgets/nutrition/charts.dart b/lib/widgets/nutrition/charts.dart index e3764bf0..cf7b74c9 100644 --- a/lib/widgets/nutrition/charts.dart +++ b/lib/widgets/nutrition/charts.dart @@ -43,8 +43,8 @@ class FlNutritionalPlanGoalWidget extends StatefulWidget { // even if it did, i doubt it would let us put text between the gauges/bars // * LinearProgressIndicator has no way to visualize going beyond 100%, or // using multiple colors to show multiple components such as surplus, deficit -// * here we try drawing our own simple gauges that can go beyond 100%,and we -// can use multiple colors... +// * here we draw our own simple gauges that can go beyond 100%, +// and support multiple segments class FlNutritionalPlanGoalWidgetState extends State { // normWidth is the width representing 100% completion // note that if val > plan, we will draw beyond this width diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 0cce2354..cc164067 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -39,7 +39,7 @@ class MealForm extends StatelessWidget { final _nameController = TextEditingController(); MealForm(this._planId, [meal]) { - _meal = meal ?? Meal(plan: _planId); + _meal = meal ?? Meal(plan: _planId, time: TimeOfDay.fromDateTime(DateTime.now())); _timeController.text = timeToString(_meal.time)!; _nameController.text = _meal.name; } diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index 91d0f9b2..0efa2f7b 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -407,7 +407,7 @@ class MealHeader extends StatelessWidget { ], ) : Text( - _meal.time!.format(context), + _meal.time != null ? _meal.time!.format(context) : '', style: Theme.of(context).textTheme.headlineSmall, ), ), diff --git a/pubspec.lock b/pubspec.lock index 4ca9db0d..c0fa4cd1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -607,6 +607,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + golden_toolkit: + dependency: "direct dev" + description: + name: golden_toolkit + sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" + url: "https://pub.dev" + source: hosted + version: "0.15.0" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2eee6f91..21a707d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dev_dependencies: cider: ^0.2.7 drift_dev: ^2.17.0 freezed: ^2.5.2 + golden_toolkit: ^0.15.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/nutrition/goldens/nutritional_plan_1_default_view.png b/test/nutrition/goldens/nutritional_plan_1_default_view.png new file mode 100644 index 00000000..4f9eaee0 Binary files /dev/null and b/test/nutrition/goldens/nutritional_plan_1_default_view.png differ diff --git a/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png b/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png new file mode 100644 index 00000000..34719666 Binary files /dev/null and b/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png differ diff --git a/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png b/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png new file mode 100644 index 00000000..f6f2cffa Binary files /dev/null and b/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png differ diff --git a/test/nutrition/nutritional_plan_screen_test.dart b/test/nutrition/nutritional_plan_screen_test.dart index 8caeefcf..4fbd97a2 100644 --- a/test/nutrition/nutritional_plan_screen_test.dart +++ b/test/nutrition/nutritional_plan_screen_test.dart @@ -16,22 +16,112 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:http/http.dart' as http; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/base_provider.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/screens/nutritional_plan_screen.dart'; import '../../test_data/nutritional_plans.dart'; +import '../fixtures/fixture_reader.dart'; +import 'helper.dart'; import 'nutritional_plan_screen_test.mocks.dart'; @GenerateMocks([WgerBaseProvider, AuthProvider, http.Client]) void main() { + // makeUrl('nutritionplan', {id: 1, objectMethod: null, query: null}) + // Add a stub for this method using Mockito's 'when' API, or generate the MockWgerBaseProvider mock with the @GenerateNiceMocks annotation (see https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html).) + + late NutritionPlansProvider nutritionProvider; + late MockWgerBaseProvider mockWgerBaseProvider; + + setUp(() { + mockWgerBaseProvider = MockWgerBaseProvider(); + nutritionProvider = NutritionPlansProvider(mockWgerBaseProvider, []); + + const String planInfoUrl = 'nutritionplaninfo'; + const String planUrl = 'nutritionplan'; + const String diaryUrl = 'nutritiondiary'; + const String ingredientUrl = 'ingredient'; + + final Map nutritionalPlanInfoResponse = jsonDecode( + fixture('nutrition/nutritional_plan_info_detail_response.json'), + ); + final Map nutritionalPlanDetailResponse = jsonDecode( + fixture('nutrition/nutritional_plan_detail_response.json'), + ); + final List nutritionDiaryResponse = jsonDecode( + fixture('nutrition/nutrition_diary_response.json'), + )['results']; + final Map ingredient59887Response = jsonDecode( + fixture('nutrition/ingredient_59887_response.json'), + ); + final Map ingredient10065Response = jsonDecode( + fixture('nutrition/ingredient_10065_response.json'), + ); + final Map ingredient58300Response = jsonDecode( + fixture('nutrition/ingredient_58300_response.json'), + ); + + final ingredientList = [ + Ingredient.fromJson(ingredient59887Response), + Ingredient.fromJson(ingredient10065Response), + Ingredient.fromJson(ingredient58300Response), + ]; + + nutritionProvider.ingredients = ingredientList; + + final Uri planInfoUri = Uri( + scheme: 'http', + host: 'localhost', + path: 'api/v2/$planInfoUrl/1', + ); + final Uri planUri = Uri( + scheme: 'http', + host: 'localhost', + path: 'api/v2/$planUrl', + ); + final Uri diaryUri = Uri( + scheme: 'http', + host: 'localhost', + path: 'api/v2/$diaryUrl', + ); + when(mockWgerBaseProvider.makeUrl(planInfoUrl, id: anyNamed('id'))).thenReturn(planInfoUri); + when(mockWgerBaseProvider.makeUrl(planUrl, id: anyNamed('id'))).thenReturn(planUri); + when(mockWgerBaseProvider.makeUrl(diaryUrl, query: anyNamed('query'))).thenReturn(diaryUri); + when(mockWgerBaseProvider.fetch(planInfoUri)).thenAnswer( + (realInvocation) => Future.value(nutritionalPlanInfoResponse), + ); + when(mockWgerBaseProvider.fetch(planUri)).thenAnswer( + (realInvocation) => Future.value(nutritionalPlanDetailResponse), + ); + when(mockWgerBaseProvider.fetchPaginated(diaryUri)).thenAnswer( + (realInvocation) => Future.value(nutritionDiaryResponse), + ); + }); + +/* + late MockWgerBaseProvider mockBaseProvider; +// late ExercisesProvider provider; + + const String npPlanUrl = 'nutritionplan'; + + final Uri tNpPlanEntriesUri = Uri( + scheme: 'http', + host: 'localhost', + path: 'api/v2/$npPlanUrl/', + ); +*/ Widget createNutritionalPlan({locale = 'en'}) { final key = GlobalKey(); final mockBaseProvider = MockWgerBaseProvider(); @@ -65,23 +155,70 @@ void main() { ); } - testWidgets('Test the widgets on the nutritional plan screen', (WidgetTester tester) async { - await tester.pumpWidget(createNutritionalPlan()); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); + testGoldens( + 'Test the widgets on the nutritional plan screen', + (tester) async { + await loadAppFonts(); + await tester.takeScreenshot(name: 'die1'); + final globalKey = GlobalKey(); + await tester.pumpWidgetBuilder( + Material( + key: globalKey, + ), + wrapper: materialAppWrapper( + localizations: [ + AppLocalizations.delegate, + ], + ), + surfaceSize: const Size(500, 1000), + ); + await tester.pumpWidget(createNutritionalPlan()); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); - // Plan description - expect(find.text('Less fat, more protein'), findsOneWidget); + await screenMatchesGolden(tester, 'nutritional_plan_1_default_view'); - // Ingredients - expect(find.text('100g Water'), findsOneWidget); - expect(find.text('75g Burger soup'), findsOneWidget); - // the new goals widget pushes this content down a bit... - await tester.scrollUntilVisible(find.text('300g Broccoli cake'), 30); - expect(find.text('300g Broccoli cake'), findsOneWidget); + // Default view shows plan description, info button, and no ingredients + expect(find.text('Less fat, more protein'), findsOneWidget); + expect(find.byIcon(Icons.info_outline), findsNWidgets(3)); // 2 meals, 1 "other logs" + expect(find.byIcon(Icons.info), findsNothing); + expect(find.text('100g Water'), findsNothing); + expect(find.text('75g Burger soup'), findsNothing); - expect(find.byType(Card), findsNWidgets(2)); - }); + // tap the first info button changes it and reveals ingredients for the first meal + var infoOutlineButtons = find.byIcon(Icons.info_outline); + await tester.tap(infoOutlineButtons.first); // 2nd button shows up also, but is off-screen + await tester.pumpAndSettle(); + await tester.takeScreenshot(name: 'die2'); + await screenMatchesGolden(tester, 'nutritional_plan_2_one_meal_with_ingredients'); + + // Ingredients show up now + expect(find.text('100g Water'), findsOneWidget); + expect(find.text('75g Burger soup'), findsOneWidget); + + // .. and the button icon has changed + expect(find.byIcon(Icons.info_outline), findsNWidgets(2)); + expect(find.byIcon(Icons.info), findsOneWidget); + + // the goals widget pushes this content down a bit. + // let's first find our icon (note: the previous icon no longer matches) + infoOutlineButtons = find.byIcon(Icons.info_outline); + + await tester.scrollUntilVisible(infoOutlineButtons.first, 30); + expect(find.text('300g Broccoli cake'), findsNothing); + + await tester.tap(infoOutlineButtons.first); + await tester.pumpAndSettle(); + await screenMatchesGolden(tester, 'nutritional_plan_3_both_meals_with_ingredients'); + expect(find.byIcon(Icons.info_outline), findsOneWidget); + expect(find.byIcon(Icons.info), findsNWidgets(2)); + + await tester.scrollUntilVisible(find.text('300g Broccoli cake'), 30); + expect(find.text('300g Broccoli cake'), findsOneWidget); + + expect(find.byType(Card), findsNWidgets(3)); + }, + ); testWidgets('Tests the localization of times - EN', (WidgetTester tester) async { await tester.pumpWidget(createNutritionalPlan()); diff --git a/test_data/dart_test.yaml b/test_data/dart_test.yaml new file mode 100644 index 00000000..62bd2746 --- /dev/null +++ b/test_data/dart_test.yaml @@ -0,0 +1,2 @@ +tags: + golden: \ No newline at end of file