diff --git a/test/features/measurements/providers/measurement_notifier_test.dart b/test/features/measurements/providers/measurement_notifier_test.dart index 6e78a5c65..aef364810 100644 --- a/test/features/measurements/providers/measurement_notifier_test.dart +++ b/test/features/measurements/providers/measurement_notifier_test.dart @@ -40,6 +40,8 @@ void main() { when(mockRepo.updateLocalDrift(any)).thenAnswer((_) async {}); when(mockRepo.addLocalDrift(any)).thenAnswer((_) async {}); + when(mockRepo.addLocalDriftGroupEntries(any)).thenAnswer((_) async {}); + when(mockRepo.deleteLocalDriftCategory(any)).thenAnswer((_) async {}); when(mockRepo.updateLocalDriftCategory(any)).thenAnswer((_) async {}); when(mockRepo.addLocalDriftCategory(any)).thenAnswer((_) async {}); @@ -103,6 +105,23 @@ void main() { }); }); + group('addGroupEntries', () { + test('addGroupEntries calls repository', () async { + final notifier = container.read(measurementProvider.notifier); + final entries = [testNeasurementEntry9, testNeasurementEntry10]; + + await notifier.addGroupEntries(entries); + verify(mockRepo.addLocalDriftGroupEntries(entries)).called(1); + }); + + test('addGroupEntries forwards empty list to repository', () async { + final notifier = container.read(measurementProvider.notifier); + + await notifier.addGroupEntries([]); + verify(mockRepo.addLocalDriftGroupEntries([])).called(1); + }); + }); + group('setCategoryOrder', () { // Three top-level categories: Body fat ('1'), Biceps ('2') and the blood // pressure group parent ('bp'), whose children must stay untouched. diff --git a/test/features/measurements/screens/measurement_categories_screen_test.dart b/test/features/measurements/screens/measurement_categories_screen_test.dart index 03bc12dc7..5ffa0f185 100644 --- a/test/features/measurements/screens/measurement_categories_screen_test.dart +++ b/test/features/measurements/screens/measurement_categories_screen_test.dart @@ -36,7 +36,12 @@ void main() { final mockRepo = MockMeasurementRepository(); when( mockRepo.watchAll(), - ).thenAnswer((_) => Stream>.value(getMeasurementCategories())); + ).thenAnswer( + (_) => Stream>.value([ + ...getMeasurementCategories(), + ...getBloodPressureGroup(), + ]), + ); return ProviderScope( overrides: [ @@ -52,13 +57,19 @@ void main() { } testWidgets('Test the widgets on the measurement category screen', (WidgetTester tester) async { + tester.view.physicalSize = const Size(800, 2200); + tester.view.devicePixelRatio = 1.0; + await tester.pumpWidget(createMeasurementScreen()); await tester.pumpAndSettle(); expect(find.text('Measurements'), findsOneWidget); expect(find.text('Body fat'), findsOneWidget); expect(find.text('Biceps'), findsOneWidget); - expect(find.byType(Card), findsNWidgets(2)); - expect(find.byType(MeasurementChartWidgetFl), findsNWidgets(2)); + expect(find.text('Blood pressure'), findsOneWidget); + expect(find.text('Systolic'), findsOneWidget); + expect(find.text('Diastolic'), findsOneWidget); + expect(find.byType(ListTile), findsNWidgets(2)); // Systolic , Diastolic + expect(find.byType(MeasurementChartWidgetFl), findsNWidgets(2)); // Body fat, Biceps }); } diff --git a/test/features/measurements/widgets/build_chart_for_metric_type_test.dart b/test/features/measurements/widgets/build_chart_for_metric_type_test.dart new file mode 100644 index 000000000..170df0afa --- /dev/null +++ b/test/features/measurements/widgets/build_chart_for_metric_type_test.dart @@ -0,0 +1,67 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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. + * + * This program 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_test/flutter_test.dart'; +import 'package:wger/features/measurements/models/measurement_category.dart'; +import 'package:wger/features/measurements/widgets/charts.dart'; +import 'package:wger/features/measurements/widgets/helpers.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +Widget _wrapChart(Widget chart) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SizedBox(width: 400, height: 300, child: chart), + ), +); + +void main() { + final entries = [ + MeasurementChartEntry(1000, DateTime(2026, 1, 1)), + MeasurementChartEntry(2000, DateTime(2026, 1, 2)), + ]; + + group('buildChartForMetricType routing', () { + testWidgets('steps -> MeasurementBarChartWidgetFl', (tester) async { + final widget = buildChartForMetricType(MetricType.steps, entries, [], 'steps'); + await tester.pumpWidget(_wrapChart(widget)); + await tester.pumpAndSettle(); + + expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget); + expect(find.byType(MeasurementChartWidgetFl), findsNothing); + }); + + testWidgets('custom -> MeasurementChartWidgetFl', (tester) async { + final widget = buildChartForMetricType(MetricType.custom, entries, [], 'cm'); + await tester.pumpWidget(_wrapChart(widget)); + await tester.pumpAndSettle(); + + expect(find.byType(MeasurementChartWidgetFl), findsOneWidget); + expect(find.byType(MeasurementBarChartWidgetFl), findsNothing); + }); + + testWidgets('energy -> MeasurementBarChartWidgetFl', (tester) async { + final widget = buildChartForMetricType(MetricType.energy, entries, [], 'kcal'); + await tester.pumpWidget(_wrapChart(widget)); + await tester.pumpAndSettle(); + + expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget); + }); + }); +} diff --git a/test/features/measurements/widgets/categories_card_group_test.dart b/test/features/measurements/widgets/categories_card_group_test.dart new file mode 100644 index 000000000..b46d7fd51 --- /dev/null +++ b/test/features/measurements/widgets/categories_card_group_test.dart @@ -0,0 +1,72 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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. + * + * This program 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_test/flutter_test.dart'; +import 'package:wger/features/measurements/models/measurement_category.dart'; +import 'package:wger/features/measurements/models/measurement_entry.dart'; +import 'package:wger/features/measurements/widgets/categories_card.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +import '../../../../test_data/measurements.dart'; + +Widget _wrap(Widget child) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routes: { + '/form': (ctx) => const Scaffold(body: Text('form screen')), + }, + home: Scaffold(body: child), +); + +MeasurementCategory _bpGroup({bool withEntries = false}) { + final sysEntries = withEntries ? [testNeasurementEntry9] : []; + final diaEntries = withEntries ? [testNeasurementEntry10] : []; + final sys = testMeasurementCategorySystolic.copyWith(entries: sysEntries); + final dia = testMeasurementCategoryDiastolic.copyWith(entries: diaEntries); + + return testMeasurementCategoryBloodPressure.copyWith(children: [sys, dia]); +} + +void main() { + group('CategoriesCard — group card', () { + testWidgets('shows one ListTile per child with latest reading', (tester) async { + await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup(withEntries: true)))); + await tester.pumpAndSettle(); + + expect(find.byType(ListTile), findsNWidgets(2)); + expect(find.textContaining('120'), findsOneWidget); + expect(find.textContaining('80'), findsOneWidget); + }); + + testWidgets('shows dash when child has no entries', (tester) async { + await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup(withEntries: false)))); + await tester.pumpAndSettle(); + + // Text('—') should appear for both children + expect(find.text('—'), findsNWidgets(2)); + }); + + testWidgets('add icon button is present on group card', (tester) async { + await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup()))); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add), findsOneWidget); + }); + }); +} diff --git a/test/features/measurements/widgets/categories_list_test.dart b/test/features/measurements/widgets/categories_list_test.dart new file mode 100644 index 000000000..e6b2c1694 --- /dev/null +++ b/test/features/measurements/widgets/categories_list_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/features/measurements/providers/measurement_repository.dart'; +import 'package:wger/features/measurements/widgets/categories.dart'; +import 'package:wger/features/measurements/widgets/categories_card.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import '../../../../test_data/measurements.dart'; +import '../providers/measurement_notifier_test.mocks.dart'; + +Widget _wrap(MockMeasurementRepository mockRepo) { + return ProviderScope( + overrides: [ + measurementRepositoryProvider.overrideWithValue(mockRepo), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: CategoriesList()), + ), + ); +} + +void main() { + late MockMeasurementRepository mockRepo; + + setUp(() { + mockRepo = MockMeasurementRepository(); + }); + + group('CategoriesList', () { + testWidgets('two top-level categories render two CategoriesCard widgets', (tester) async { + when(mockRepo.watchAll()).thenAnswer((_) => Stream.value(getMeasurementCategories())); + + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + expect(find.byType(CategoriesCard), findsNWidgets(2)); + }); + + testWidgets(' children of multi-value groups are not rendered as own list items', ( + tester, + ) async { + // Only 'bp' should produce a CategoriesCard; children stay inside it. + when(mockRepo.watchAll()).thenAnswer((_) => Stream.value(getBloodPressureGroup())); + + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + expect(find.byType(CategoriesCard), findsOneWidget); + expect(find.text('Systolic'), findsOneWidget); + expect(find.text('Diastolic'), findsOneWidget); + }); + + testWidgets('empty list renders no CategoriesCard', (tester) async { + when(mockRepo.watchAll()).thenAnswer((_) => Stream.value([])); + + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + expect(find.byType(CategoriesCard), findsNothing); + }); + }); +} diff --git a/test/features/measurements/widgets/charts_test.dart b/test/features/measurements/widgets/charts_test.dart index 0043acfc3..73c85a75c 100644 --- a/test/features/measurements/widgets/charts_test.dart +++ b/test/features/measurements/widgets/charts_test.dart @@ -16,18 +16,51 @@ * along with this program. If not, see . */ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:wger/features/measurements/widgets/charts.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +Widget _wrap(Widget child) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SizedBox(width: 400, height: 300, child: child), + ), +); void main() { MeasurementChartEntry entry(num value, DateTime date) => MeasurementChartEntry(value, date); - group('aggregatePerDay', () { - test('returns an empty list for no entries', () { + group('MeasurementBarChartWidgetFl', () { + testWidgets('renders without error for empty entries', (tester) async { + await tester.pumpWidget( + _wrap(const MeasurementBarChartWidgetFl([], 'steps')), + ); + await tester.pumpAndSettle(); + expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + // T-24 + testWidgets('BarChart is present for non-empty entries', (tester) async { + final entries = [ + entry(1000, DateTime(2026, 1, 1)), + entry(2000, DateTime(2026, 1, 2)), + entry(1500, DateTime(2026, 1, 3)), + ]; + await tester.pumpWidget(_wrap(MeasurementBarChartWidgetFl(entries, 'steps'))); + await tester.pumpAndSettle(); + + expect(find.byType(BarChart), findsOneWidget); + }); + + test('aggregatePerDay returns an empty list for no entries', () { expect(aggregatePerDay([]), isEmpty); }); - test('sums entries sharing a calendar day into one point', () { + test('aggregatePerDay sums entries sharing a calendar day into one point', () { final result = aggregatePerDay([ entry(1000, DateTime(2026, 1, 1, 8)), entry(2500, DateTime(2026, 1, 1, 20)), diff --git a/test/features/measurements/widgets/get_overview_widgets_test.dart b/test/features/measurements/widgets/get_overview_widgets_test.dart new file mode 100644 index 000000000..8a838b199 --- /dev/null +++ b/test/features/measurements/widgets/get_overview_widgets_test.dart @@ -0,0 +1,135 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 - 2026 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. + * + * This program 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_test/flutter_test.dart'; +import 'package:wger/features/measurements/models/measurement_category.dart'; +import 'package:wger/features/measurements/widgets/charts.dart'; +import 'package:wger/features/measurements/widgets/helpers.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +void main() { + final rawEntries = [ + MeasurementChartEntry(10, DateTime(2026, 1, 1)), + MeasurementChartEntry(20, DateTime(2026, 1, 10)), + ]; + final avgEntries = moving7dAverage(rawEntries); + + group('getOverviewWidgets', () { + testWidgets('empty raw shows no-data placeholder', (tester) async { + late List widgets; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (ctx) { + widgets = getOverviewWidgets('Test', [], [], 'cm', ctx); + return Scaffold( + body: SingleChildScrollView(child: Column(children: widgets)), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('No data available'), findsOneWidget); + }); + + testWidgets('non-empty avg includes MeasurementOverallChangeWidget', (tester) async { + late List widgets; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (ctx) { + widgets = getOverviewWidgets('Test', rawEntries, avgEntries, 'cm', ctx); + return Scaffold( + body: SingleChildScrollView(child: Column(children: widgets)), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(MeasurementOverallChangeWidget), findsOneWidget); + }); + }); + + group('getOverviewWidgetsSeries — Indicator legend', () { + testWidgets('three Indicator widgets for non-summed metric (custom)', (tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (ctx) { + final widgets = getOverviewWidgetsSeries( + 'Weight', + rawEntries, + avgEntries, + [], + 'kg', + ctx, + metricType: MetricType.custom, + ); + return Scaffold( + body: SingleChildScrollView(child: Column(children: widgets)), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(Indicator), findsNWidgets(3)); + }); + + testWidgets('two Indicator widgets for summed metric (steps — no trend)', (tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (ctx) { + final widgets = getOverviewWidgetsSeries( + 'Steps', + rawEntries, + avgEntries, + [], + 'steps', + ctx, + metricType: MetricType.steps, + ); + return Scaffold( + body: SingleChildScrollView(child: Column(children: widgets)), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + // isSummedPerDay -> trend indicator is hidden + expect(find.byType(Indicator), findsNWidgets(2)); + }); + }); +} diff --git a/test/features/measurements/widgets/group_measurement_entry_form_test.dart b/test/features/measurements/widgets/group_measurement_entry_form_test.dart new file mode 100644 index 000000000..971b60e22 --- /dev/null +++ b/test/features/measurements/widgets/group_measurement_entry_form_test.dart @@ -0,0 +1,96 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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. + * + * This program 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_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/core/widgets/decimal_input.dart'; +import 'package:wger/features/measurements/providers/measurement_repository.dart'; +import 'package:wger/features/measurements/widgets/forms.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +import '../../../../test_data/measurements.dart'; +import '../providers/measurement_notifier_test.mocks.dart'; + +Widget _wrap(MockMeasurementRepository mockRepo) { + return ProviderScope( + overrides: [ + measurementRepositoryProvider.overrideWithValue(mockRepo), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SingleChildScrollView( + child: GroupMeasurementEntryForm(testMeasurementCategoryBloodPressure), + ), + ), + ), + ); +} + +void main() { + late MockMeasurementRepository mockRepo; + + setUp(() { + mockRepo = MockMeasurementRepository(); + when(mockRepo.watchAll()).thenAnswer((_) => Stream.value([])); + when(mockRepo.addLocalDriftGroupEntries(any)).thenAnswer((_) async {}); + }); + + group('GroupMeasurementEntryForm', () { + testWidgets('renders one DecimalInputWidget per child component', (tester) async { + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + expect(find.byType(DecimalInputWidget), findsNWidgets(2)); + }); + + testWidgets('empty value fields fail validation', (tester) async { + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + // Tap save without entering values + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + // Form doesnot pop + expect(find.byType(GroupMeasurementEntryForm), findsOneWidget); + }); + + testWidgets('valid submission calls addGroupEntries with correct count', (tester) async { + await tester.pumpWidget(_wrap(mockRepo)); + await tester.pumpAndSettle(); + + final fields = find.byType(TextFormField); + // Fields : Date, Time, Systolic value, Diastolic value + await tester.enterText(fields.at(2), '120'); + await tester.enterText(fields.at(3), '80'); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + verify( + mockRepo.addLocalDriftGroupEntries( + argThat(hasLength(2)), + ), + ).called(1); + }); + }); +} diff --git a/test/features/measurements/widgets/measurement_category_form_test.dart b/test/features/measurements/widgets/measurement_category_form_test.dart new file mode 100644 index 000000000..45098b9f9 --- /dev/null +++ b/test/features/measurements/widgets/measurement_category_form_test.dart @@ -0,0 +1,114 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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. + * + * This program 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_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/features/measurements/models/measurement_category.dart'; +import 'package:wger/features/measurements/providers/measurement_repository.dart'; +import 'package:wger/features/measurements/widgets/forms.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; + +import '../../../../test_data/measurements.dart'; +import '../providers/measurement_notifier_test.mocks.dart'; + +Widget _wrap(Widget child, MockMeasurementRepository mockRepo) { + return ProviderScope( + overrides: [ + measurementRepositoryProvider.overrideWithValue(mockRepo), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SingleChildScrollView(child: child), + ), + ), + ); +} + +void main() { + late MockMeasurementRepository mockRepo; + + setUp(() { + mockRepo = MockMeasurementRepository(); + when(mockRepo.watchAll()).thenAnswer((_) => Stream.value([])); + }); + + group('MeasurementCategoryForm validation', () { + testWidgets('empty name fails validation', (tester) async { + await tester.pumpWidget(_wrap(const MeasurementCategoryForm(), mockRepo)); + await tester.pumpAndSettle(); + + // Tap save without entering any text + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + // Check for validation errors. + final error = find.byWidgetPredicate((widget) { + if (widget is TextField) { + return widget.decoration?.errorText != null; + } + return false; + }); + expect(error, findsAtLeastNWidgets(1)); + }); + + testWidgets('empty unit field fails validation', (tester) async { + await tester.pumpWidget(_wrap(const MeasurementCategoryForm(), mockRepo)); + await tester.pumpAndSettle(); + + // Fill name but leave unit empty + await tester.enterText(find.byType(TextFormField).first, 'Body fat'); + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + // still on same screen + expect(find.byType(MeasurementCategoryForm), findsOneWidget); + }); + + testWidgets('metricType dropdown renders all MetricType values', (tester) async { + await tester.pumpWidget(_wrap(const MeasurementCategoryForm(), mockRepo)); + await tester.pumpAndSettle(); + + // Open the dropdown + await tester.tap(find.byType(DropdownButtonFormField)); + await tester.pumpAndSettle(); + + // We expect N + 1 items here because when the dropdown is opened: + // 1. The initialValue is still displayed on the form background as a DropdownMenuItem. + // 2. The opened menu overlay displays all 6 total MetricType options. + // Total = 1 (background label) + 6 (popup menu options) = 7 widgets. + expect( + find.byType(DropdownMenuItem), + findsNWidgets(MetricType.values.length + 1), + ); + }); + + testWidgets('editing existing category pre-fills name and unit', (tester) async { + final existing = getMeasurementCategories()[1]; + + await tester.pumpWidget(_wrap(MeasurementCategoryForm(existing), mockRepo)); + await tester.pumpAndSettle(); + + expect(find.widgetWithText(TextFormField, 'Biceps'), findsOneWidget); + expect(find.widgetWithText(TextFormField, 'cm'), findsOneWidget); + }); + }); +} diff --git a/test_data/measurements.dart b/test_data/measurements.dart index 73c237751..f26e18492 100644 --- a/test_data/measurements.dart +++ b/test_data/measurements.dart @@ -78,12 +78,29 @@ final testMeasurementEntry8 = MeasurementEntry( notes: '', ); +final testNeasurementEntry9 = MeasurementEntry( + id: 'e1', + categoryId: 'sys', + date: DateTime(2026, 1, 1), + value: 120, + notes: '', +); + +final testNeasurementEntry10 = MeasurementEntry( + id: 'e2', + categoryId: 'dia', + date: DateTime(2026, 1, 1), + value: 80, + notes: '', +); + /// Multi-value group: blood pressure with systolic/diastolic components. final testMeasurementCategoryBloodPressure = MeasurementCategory( id: 'bp', name: 'Blood pressure', unit: 'mmHg', metricType: MetricType.bloodPressure, + children: [testMeasurementCategorySystolic, testMeasurementCategoryDiastolic], ); final testMeasurementCategorySystolic = MeasurementCategory(