diff --git a/lib/widgets/routines/forms/reps_unit.dart b/lib/widgets/routines/forms/reps_unit.dart index 70e53c3b..f19d7e9f 100644 --- a/lib/widgets/routines/forms/reps_unit.dart +++ b/lib/widgets/routines/forms/reps_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 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, + * 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. @@ -29,7 +29,7 @@ class RepetitionUnitInputWidget extends StatefulWidget { late int? selectedRepetitionUnit; final ValueChanged onChanged; - RepetitionUnitInputWidget(initialValue, {required this.onChanged}) { + RepetitionUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedRepetitionUnit = initialValue; } @@ -47,7 +47,7 @@ class _RepetitionUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration( labelText: AppLocalizations.of(context).repetitionUnit, ), diff --git a/lib/widgets/routines/forms/rir.dart b/lib/widgets/routines/forms/rir.dart index 22c95753..da406659 100644 --- a/lib/widgets/routines/forms/rir.dart +++ b/lib/widgets/routines/forms/rir.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 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, + * 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. @@ -30,7 +30,7 @@ class RiRInputWidget extends StatefulWidget { static const SLIDER_START = -0.5; - RiRInputWidget(this._initialValue, {required this.onChanged}) { + RiRInputWidget(this._initialValue, {super.key, required this.onChanged}) { _logger.finer('Initializing with initial value: $_initialValue'); } diff --git a/lib/widgets/routines/forms/weight_unit.dart b/lib/widgets/routines/forms/weight_unit.dart index 8afb6784..751c6e40 100644 --- a/lib/widgets/routines/forms/weight_unit.dart +++ b/lib/widgets/routines/forms/weight_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 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, + * 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. @@ -29,7 +29,7 @@ class WeightUnitInputWidget extends StatefulWidget { late int? selectedWeightUnit; final ValueChanged onChanged; - WeightUnitInputWidget(int? initialValue, {required this.onChanged}) { + WeightUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedWeightUnit = initialValue; } @@ -47,7 +47,7 @@ class _WeightUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration(labelText: AppLocalizations.of(context).weightUnit), onChanged: (WeightUnit? newValue) { setState(() { diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index d8ce9e04..70254266 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2025 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, + * 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. @@ -661,6 +661,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( + key: const ValueKey('logs-reps-widget'), controller: _repetitionsController, configData: widget.configData, focusNode: widget.focusNode, @@ -673,6 +674,7 @@ class _LogFormWidgetState extends ConsumerState { const SizedBox(width: 8), Flexible( child: LogsWeightWidget( + key: const ValueKey('logs-weight-widget'), controller: _weightController, configData: widget.configData, focusNode: widget.focusNode, @@ -690,6 +692,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( + key: const ValueKey('logs-reps-widget'), controller: _repetitionsController, configData: widget.configData, focusNode: widget.focusNode, @@ -702,6 +705,7 @@ class _LogFormWidgetState extends ConsumerState { const SizedBox(width: 8), Flexible( child: RepetitionUnitInputWidget( + key: const ValueKey('repetition-unit-input-widget'), _log.repetitionsUnitId, onChanged: (v) => {}, ), @@ -715,6 +719,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsWeightWidget( + key: const ValueKey('logs-weight-widget'), controller: _weightController, configData: widget.configData, focusNode: widget.focusNode, @@ -726,13 +731,18 @@ class _LogFormWidgetState extends ConsumerState { ), const SizedBox(width: 8), Flexible( - child: WeightUnitInputWidget(_log.weightUnitId, onChanged: (v) => {}), + child: WeightUnitInputWidget( + _log.weightUnitId, + onChanged: (v) => {}, + key: const ValueKey('weight-unit-input-widget'), + ), ), const SizedBox(width: 8), ], ), if (_detailed) RiRInputWidget( + key: const ValueKey('rir-input-widget'), _log.rir, onChanged: (value) { if (value == '') { @@ -743,6 +753,7 @@ class _LogFormWidgetState extends ConsumerState { }, ), SwitchListTile( + key: const ValueKey('units-switch'), dense: true, title: Text(i18n.setUnitsAndRir), value: _detailed, @@ -753,6 +764,7 @@ class _LogFormWidgetState extends ConsumerState { }, ), FilledButton( + key: const ValueKey('save-log-button'), onPressed: _isSaving ? null : () async { diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index b3fa5c8b..45526e7f 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -20,12 +20,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart' as provider; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/day_data.dart'; +import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/set_config_data.dart'; import 'package:wger/models/workouts/slot_data.dart'; import 'package:wger/providers/exercises.dart'; @@ -41,7 +43,7 @@ import 'log_page_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('LogPage smoke tests', () { + group('LogPage tests', () { late List testExercises; late ProviderContainer container; @@ -51,18 +53,29 @@ void main() { container = ProviderContainer.test(); }); - Future pumpLogPage(WidgetTester tester) async { + Future pumpLogPage(WidgetTester tester, {RoutinesProvider? routinesProvider}) async { + final providerValue = routinesProvider ?? MockRoutinesProvider(); + await tester.pumpWidget( UncontrolledProviderScope( container: container, child: provider.ChangeNotifierProvider.value( - value: MockRoutinesProvider(), + value: providerValue, child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( - body: LogPage(PageController()), + // Provide a PageView so the PageController used by LogPage is attached + body: Builder( + builder: (context) { + final controller = PageController(); + return PageView( + controller: controller, + children: [LogPage(controller)], + ); + }, + ), ), ), ), @@ -135,5 +148,91 @@ void main() { expect(find.byType(LogPage), findsOneWidget); }); + + testWidgets('copy from past log updates form fields and shows SnackBar', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + + // Act + // Log page is at index 2 + notifier.state = notifier.state.copyWith(currentPage: 2); + expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); + await pumpLogPage(tester); + + // Assert + final pastLogTile = find.byKey(const ValueKey('past-log-1')); + expect(pastLogTile, findsOneWidget); + await tester.tap(pastLogTile); + await tester.pumpAndSettle(); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + // Get controller texts + final repControllerText = tester.widget(editableFields.at(0)).controller.text; + final weightControllerText = tester + .widget(editableFields.at(1)) + .controller + .text; + + expect(repControllerText, contains('10')); + expect(weightControllerText, contains('10')); + expect(find.byType(SnackBar), findsOneWidget); + }); + + testWidgets('save button calls addLog on RoutinesProvider', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.state = notifier.state.copyWith(currentPage: 2); + final mockRoutines = MockRoutinesProvider(); + + // Act + await pumpLogPage(tester, routinesProvider: mockRoutines); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + await tester.enterText(editableFields.at(0), '7'); + await tester.enterText(editableFields.at(1), '77'); + await tester.pumpAndSettle(); + + Log? capturedLog; + when(mockRoutines.addLog(any)).thenAnswer((invocation) async { + capturedLog = invocation.positionalArguments[0] as Log; + capturedLog!.id = 42; + return capturedLog!; + }); + + final saveButton = find.byKey(const ValueKey('save-log-button')); + expect(saveButton, findsOneWidget); + + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + // Assert + verify(mockRoutines.addLog(any)).called(1); + expect(capturedLog, isNotNull); + expect(capturedLog!.repetitions, equals(7)); + expect(capturedLog!.weight, equals(77)); + + final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; + expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); + expect(capturedLog!.routineId, equals(notifier.state.routine.id)); + expect(capturedLog!.iteration, equals(notifier.state.iteration)); + }); }); }