diff --git a/lib/models/workouts/session.dart b/lib/models/workouts/session.dart index 78ef47c4..f0888781 100644 --- a/lib/models/workouts/session.dart +++ b/lib/models/workouts/session.dart @@ -33,6 +33,9 @@ class WorkoutSession { @JsonKey(required: true, name: 'routine') late int routineId; + @JsonKey(required: true, name: 'day') + int? dayId; + @JsonKey(required: true, toJson: dateToYYYYMMDD) late DateTime date; @@ -53,6 +56,7 @@ class WorkoutSession { WorkoutSession({ this.id, + this.dayId, required this.routineId, this.impression = 2, this.notes = '', diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index 63444c65..534cb097 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -9,10 +9,11 @@ part of 'session.dart'; WorkoutSession _$WorkoutSessionFromJson(Map json) { $checkKeys( json, - requiredKeys: const ['id', 'routine', 'date', 'impression', 'time_start', 'time_end'], + requiredKeys: const ['id', 'routine', 'day', 'date', 'impression', 'time_start', 'time_end'], ); return WorkoutSession( id: (json['id'] as num?)?.toInt(), + dayId: (json['day'] as num?)?.toInt(), routineId: (json['routine'] as num).toInt(), impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String), notes: json['notes'] as String? ?? '', @@ -29,6 +30,7 @@ WorkoutSession _$WorkoutSessionFromJson(Map json) { Map _$WorkoutSessionToJson(WorkoutSession instance) => { 'id': instance.id, 'routine': instance.routineId, + 'day': instance.dayId, 'date': dateToYYYYMMDD(instance.date), 'impression': numToString(instance.impression), 'notes': instance.notes, diff --git a/lib/widgets/routines/forms/session.dart b/lib/widgets/routines/forms/session.dart new file mode 100644 index 00000000..79d0006e --- /dev/null +++ b/lib/widgets/routines/forms/session.dart @@ -0,0 +1,240 @@ +/* + * 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:clock/clock.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/json.dart'; +import 'package:wger/helpers/ui.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/workouts/session.dart'; +import 'package:wger/providers/routines.dart'; + +class SessionForm extends StatefulWidget { + final WorkoutSession _session; + final int _routineId; + final Function()? _onSaved; + + static const SLIDER_START = -0.5; + + SessionForm(this._routineId, {Function()? onSaved, WorkoutSession? session, int? dayId}) + : _onSaved = onSaved, + _session = session ?? + WorkoutSession( + routineId: _routineId, + dayId: dayId, + impression: DEFAULT_IMPRESSION, + date: clock.now(), + timeEnd: TimeOfDay.now(), + timeStart: TimeOfDay.now(), + ); + + @override + _SessionFormState createState() => _SessionFormState(); +} + +class _SessionFormState extends State { + final _form = GlobalKey(); + + final impressionController = TextEditingController(); + final notesController = TextEditingController(); + final timeStartController = TextEditingController(); + final timeEndController = TextEditingController(); + + /// Selected impression: bad, neutral, good + var selectedImpression = [false, false, false]; + + @override + void initState() { + super.initState(); + + timeStartController.text = timeToString(widget._session.timeStart) ?? ''; + timeEndController.text = timeToString(widget._session.timeEnd) ?? ''; + notesController.text = widget._session.notes; + + selectedImpression[widget._session.impression - 1] = true; + } + + @override + void dispose() { + impressionController.dispose(); + notesController.dispose(); + timeStartController.dispose(); + timeEndController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final routinesProvider = context.read(); + + return Form( + key: _form, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToggleButtons( + renderBorder: false, + onPressed: (int index) { + setState(() { + for (int buttonIndex = 0; buttonIndex < selectedImpression.length; buttonIndex++) { + widget._session.impression = index + 1; + + if (buttonIndex == index) { + selectedImpression[buttonIndex] = true; + } else { + selectedImpression[buttonIndex] = false; + } + } + }); + }, + isSelected: selectedImpression, + children: const [ + Icon(Icons.sentiment_very_dissatisfied), + Icon(Icons.sentiment_neutral), + Icon(Icons.sentiment_very_satisfied), + ], + ), + TextFormField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context).notes, + ), + maxLines: 3, + controller: notesController, + keyboardType: TextInputType.multiline, + onFieldSubmitted: (_) {}, + onSaved: (newValue) { + widget._session.notes = newValue!; + }, + ), + Row( + children: [ + Flexible( + child: TextFormField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context).timeStart, + errorMaxLines: 2, + ), + controller: timeStartController, + onFieldSubmitted: (_) {}, + onTap: () async { + // Stop keyboard from appearing + FocusScope.of(context).requestFocus(FocusNode()); + + // Open time picker + final pickedTime = await showTimePicker( + context: context, + initialTime: widget._session.timeStart ?? TimeOfDay.now(), + ); + + if (pickedTime != null) { + timeStartController.text = timeToString(pickedTime)!; + widget._session.timeStart = pickedTime; + } + }, + onSaved: (newValue) { + if (newValue != null && newValue.isNotEmpty) { + widget._session.timeStart = stringToTime(newValue); + } + }, + validator: (_) { + if (timeStartController.text.isEmpty && timeEndController.text.isEmpty) { + return null; + } + final TimeOfDay startTime = stringToTime(timeStartController.text); + final TimeOfDay endTime = stringToTime(timeEndController.text); + if (startTime.isAfter(endTime)) { + return AppLocalizations.of(context).timeStartAhead; + } + return null; + }, + ), + ), + const SizedBox(width: 10), + Flexible( + child: TextFormField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context).timeEnd, + ), + controller: timeEndController, + onFieldSubmitted: (_) {}, + onTap: () async { + // Stop keyboard from appearing + FocusScope.of(context).requestFocus(FocusNode()); + + // Open time picker + final pickedTime = await showTimePicker( + context: context, + initialTime: widget._session.timeEnd ?? TimeOfDay.now(), + ); + + if (pickedTime != null) { + timeEndController.text = timeToString(pickedTime)!; + widget._session.timeEnd = pickedTime; + } + }, + onSaved: (newValue) { + if (newValue != null && newValue.isNotEmpty) { + widget._session.timeEnd = stringToTime(newValue); + } + }, + ), + ), + ], + ), + const SizedBox(height: 5), + ElevatedButton( + key: const ValueKey('save-button'), + child: Text(AppLocalizations.of(context).save), + onPressed: () async { + // Validate and save the current values to the weightEntry + final isValid = _form.currentState!.validate(); + if (!isValid) { + return; + } + _form.currentState!.save(); + + // Save the entry on the server + try { + if (widget._session.id == null) { + await routinesProvider.addSession(widget._session, widget._routineId); + } else { + await routinesProvider.editSession(widget._session); + } + + if (context.mounted && widget._onSaved != null) { + widget._onSaved!(); + } + } on WgerHttpException catch (error) { + if (context.mounted) { + showHttpExceptionErrorDialog(error, context); + } + } catch (error) { + if (context.mounted) { + showErrorDialog(error, context); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/routines/gym_mode/gym_mode.dart b/lib/widgets/routines/gym_mode/gym_mode.dart index 44abd86b..3a82895f 100644 --- a/lib/widgets/routines/gym_mode/gym_mode.dart +++ b/lib/widgets/routines/gym_mode/gym_mode.dart @@ -194,6 +194,7 @@ class _GymModeState extends ConsumerState { _controller, ref.read(gymStateProvider).startTime, _exercisePages, + dayId: widget._dayDataGym.day!.id!, ), ]; diff --git a/lib/widgets/routines/gym_mode/session_page.dart b/lib/widgets/routines/gym_mode/session_page.dart index 93ac2b14..ce9c0f90 100644 --- a/lib/widgets/routines/gym_mode/session_page.dart +++ b/lib/widgets/routines/gym_mode/session_page.dart @@ -17,240 +17,59 @@ */ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart' as provider; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; -import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/misc.dart'; -import 'package:wger/helpers/ui.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/session.dart'; -import 'package:wger/providers/routines.dart'; +import 'package:wger/widgets/routines/forms/session.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; -class SessionPage extends StatefulWidget { +class SessionPage extends StatelessWidget { final Routine _routine; - late WorkoutSession _session; + final WorkoutSession _session; final PageController _controller; final Map _exercisePages; - final TimeOfDay _start; SessionPage( this._routine, this._controller, - this._start, - this._exercisePages, - ) { - _session = _routine.sessions.map((sessionApi) => sessionApi.session).firstWhere( - (session) => session.date.isSameDayAs(clock.now()), - orElse: () => WorkoutSession( - routineId: _routine.id!, - impression: DEFAULT_IMPRESSION, - date: clock.now(), - timeEnd: TimeOfDay.now(), - timeStart: _start, - ), - ); - } - - @override - _SessionPageState createState() => _SessionPageState(); -} - -class _SessionPageState extends State { - final _form = GlobalKey(); - final impressionController = TextEditingController(); - final notesController = TextEditingController(); - final timeStartController = TextEditingController(); - final timeEndController = TextEditingController(); - - /// Selected impression: bad, neutral, good - var selectedImpression = [false, false, false]; - - @override - void initState() { - super.initState(); - - timeStartController.text = timeToString(widget._session.timeStart ?? widget._start)!; - timeEndController.text = timeToString(widget._session.timeEnd ?? TimeOfDay.now())!; - notesController.text = widget._session.notes; - - selectedImpression[widget._session.impression - 1] = true; - } - - @override - void dispose() { - impressionController.dispose(); - notesController.dispose(); - timeStartController.dispose(); - timeEndController.dispose(); - super.dispose(); - } + TimeOfDay start, + this._exercisePages, { + int? dayId, + }) : _session = _routine.sessions.map((sessionApi) => sessionApi.session).firstWhere( + (session) => session.date.isSameDayAs(clock.now()), + orElse: () => WorkoutSession( + dayId: dayId, + routineId: _routine.id!, + impression: DEFAULT_IMPRESSION, + date: clock.now(), + timeEnd: TimeOfDay.now(), + timeStart: start, + ), + ); @override Widget build(BuildContext context) { - final routinesProvider = context.read(); - return Column( children: [ NavigationHeader( AppLocalizations.of(context).workoutSession, - widget._controller, - exercisePages: widget._exercisePages, + _controller, + exercisePages: _exercisePages, ), const Divider(), Expanded(child: Container()), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: Form( - key: _form, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ToggleButtons( - renderBorder: false, - onPressed: (int index) { - setState(() { - for (int buttonIndex = 0; - buttonIndex < selectedImpression.length; - buttonIndex++) { - widget._session.impression = index + 1; - - if (buttonIndex == index) { - selectedImpression[buttonIndex] = true; - } else { - selectedImpression[buttonIndex] = false; - } - } - }); - }, - isSelected: selectedImpression, - children: const [ - Icon(Icons.sentiment_very_dissatisfied), - Icon(Icons.sentiment_neutral), - Icon(Icons.sentiment_very_satisfied), - ], - ), - TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).notes, - ), - maxLines: 3, - controller: notesController, - keyboardType: TextInputType.multiline, - onFieldSubmitted: (_) {}, - onSaved: (newValue) { - widget._session.notes = newValue!; - }, - ), - Row( - children: [ - Flexible( - child: TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).timeStart, - errorMaxLines: 2, - ), - controller: timeStartController, - onFieldSubmitted: (_) {}, - onTap: () async { - // Stop keyboard from appearing - FocusScope.of(context).requestFocus(FocusNode()); - - // Open time picker - final pickedTime = await showTimePicker( - context: context, - initialTime: widget._session.timeStart ?? TimeOfDay.now(), - ); - - if (pickedTime != null) { - timeStartController.text = timeToString(pickedTime)!; - widget._session.timeStart = pickedTime; - } - }, - onSaved: (newValue) { - widget._session.timeStart = stringToTime(newValue); - }, - validator: (_) { - final TimeOfDay startTime = stringToTime(timeStartController.text); - final TimeOfDay endTime = stringToTime(timeEndController.text); - if (startTime.isAfter(endTime)) { - return AppLocalizations.of(context).timeStartAhead; - } - return null; - }, - ), - ), - const SizedBox(width: 10), - Flexible( - child: TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).timeEnd, - ), - controller: timeEndController, - onFieldSubmitted: (_) {}, - onTap: () async { - // Stop keyboard from appearing - FocusScope.of(context).requestFocus(FocusNode()); - - // Open time picker - final pickedTime = await showTimePicker( - context: context, - initialTime: widget._session.timeEnd ?? TimeOfDay.now(), - ); - - if (pickedTime != null) { - timeEndController.text = timeToString(pickedTime)!; - widget._session.timeEnd = pickedTime; - } - }, - onSaved: (newValue) { - widget._session.timeEnd = stringToTime(newValue); - }, - ), - ), - ], - ), - ElevatedButton( - key: const ValueKey('save-button'), - child: Text(AppLocalizations.of(context).save), - onPressed: () async { - // Validate and save the current values to the weightEntry - final isValid = _form.currentState!.validate(); - if (!isValid) { - return; - } - _form.currentState!.save(); - - // Save the entry on the server - try { - if (widget._session.id == null) { - await routinesProvider.addSession(widget._session, widget._routine.id!); - } else { - await routinesProvider.editSession(widget._session); - } - - if (mounted) { - Navigator.of(context).pop(); - } - } on WgerHttpException catch (error) { - if (mounted) { - showHttpExceptionErrorDialog(error, context); - } - } catch (error) { - if (mounted) { - showErrorDialog(error, context); - } - } - }, - ), - ], - ), + child: SessionForm( + _routine.id!, + onSaved: () => Navigator.of(context).pop(), + session: _session, ), ), - NavigationFooter(widget._controller, 1, showNext: false), + NavigationFooter(_controller, 1, showNext: false), ], ); } diff --git a/lib/widgets/routines/log.dart b/lib/widgets/routines/log.dart index 7cf88198..f4318ccf 100644 --- a/lib/widgets/routines/log.dart +++ b/lib/widgets/routines/log.dart @@ -18,73 +18,87 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; import 'package:wger/helpers/colors.dart'; -import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/misc.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/session.dart'; -import 'package:wger/providers/routines.dart'; import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/routines/charts.dart'; +import 'package:wger/widgets/routines/forms/session.dart'; -class SessionInfo extends StatelessWidget { +class SessionInfo extends StatefulWidget { final WorkoutSession _session; const SessionInfo(this._session); + @override + State createState() => _SessionInfoState(); +} + +class _SessionInfoState extends State { + bool editMode = false; + @override Widget build(BuildContext context) { final i18n = AppLocalizations.of(context); - return GestureDetector( - onTap: (){ - final routinesProvider = context.read(); - showEditSessionDialog(context, _session, routinesProvider); - - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Text( + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + ListTile( + title: Text( i18n.workoutSession, style: Theme.of(context).textTheme.headlineSmall, ), - Text( - DateFormat.yMd(Localizations.localeOf(context).languageCode).format(_session.date), - style: Theme.of(context).textTheme.headlineSmall, + subtitle: Text( + DateFormat.yMd(Localizations.localeOf(context).languageCode) + .format(widget._session.date), ), - const SizedBox(height: 8.0), - _buildInfoRow( - context, - i18n.timeStart, - _session.timeStart != null - ? MaterialLocalizations.of(context).formatTimeOfDay(_session.timeStart!) - : '-/-', + onTap: () => setState(() => editMode = !editMode), + trailing: Icon(editMode ? Icons.edit_off : Icons.edit), + contentPadding: EdgeInsets.zero, + ), + if (editMode) + SessionForm( + widget._session.routineId, + onSaved: () => setState(() => editMode = false), + session: widget._session, + ) + else + Column( + children: [ + _buildInfoRow( + context, + i18n.timeStart, + widget._session.timeStart != null + ? MaterialLocalizations.of(context) + .formatTimeOfDay(widget._session.timeStart!) + : '-/-', + ), + _buildInfoRow( + context, + i18n.timeEnd, + widget._session.timeEnd != null + ? MaterialLocalizations.of(context).formatTimeOfDay(widget._session.timeEnd!) + : '-/-', + ), + _buildInfoRow( + context, + i18n.impression, + widget._session.impressionAsString, + ), + _buildInfoRow( + context, + i18n.notes, + widget._session.notes.isNotEmpty ? widget._session.notes : '-/-', + ), + ], ), - _buildInfoRow( - context, - i18n.timeEnd, - _session.timeEnd != null - ? MaterialLocalizations.of(context).formatTimeOfDay(_session.timeEnd!) - : '-/-', - ), - _buildInfoRow( - context, - i18n.impression, - _session.impressionAsString, - ), - _buildInfoRow( - context, - i18n.notes, - _session.notes.isNotEmpty ? _session.notes : '-/-', - ), - ], - ), + ], ), ); } @@ -99,12 +113,7 @@ class SessionInfo extends StatelessWidget { '$label: ', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), - Expanded( - child: Text( - value, - style: Theme.of(context).textTheme.titleMedium, - ), - ), + Expanded(child: Text(value)), ], ), ); @@ -207,114 +216,3 @@ class DayLogWidget extends StatelessWidget { ); } } -void showEditSessionDialog(BuildContext context, WorkoutSession session, RoutinesProvider routinesProvider) { - final _formKey = GlobalKey(); - final notesController = TextEditingController(text: session.notes); - final timeStartController = TextEditingController(text: timeToString(session.timeStart ?? TimeOfDay.now())!); - final timeEndController = TextEditingController(text: timeToString(session.timeEnd ?? TimeOfDay.now())!); - List selectedImpression = [false, false, false]; - selectedImpression[session.impression - 1] = true; - - showDialog( - context: context, - builder: (ctx) { - return AlertDialog( - title: Text('Edit Session (${session.date.toLocal().toString().split(' ')[0]})'), - content: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ToggleButtons( - renderBorder: false, - isSelected: selectedImpression, - onPressed: (index) { - for (int i = 0; i < selectedImpression.length; i++) { - selectedImpression[i] = (i == index); - } - session.impression = index + 1; - }, - children: const [ - Icon(Icons.sentiment_very_dissatisfied), - Icon(Icons.sentiment_neutral), - Icon(Icons.sentiment_very_satisfied), - ], - ), - const SizedBox(height: 10), - TextFormField( - decoration: const InputDecoration(labelText: 'Notes'), - controller: notesController, - maxLines: 3, - onSaved: (value) => session.notes = value ?? '', - ), - const SizedBox(height: 10), - TextFormField( - decoration: const InputDecoration(labelText: 'Start Time'), - controller: timeStartController, - onTap: () async { - FocusScope.of(ctx).requestFocus(FocusNode()); - final picked = await showTimePicker( - context: ctx, - initialTime: session.timeStart ?? TimeOfDay.now(), - ); - if (picked != null) { - timeStartController.text = timeToString(picked)!; - session.timeStart = picked; - } - }, - validator: (_) { - final start = stringToTime(timeStartController.text); - final end = stringToTime(timeEndController.text); - if (start.isAfter(end)) { - return 'Start time cannot be after end time.'; - } - return null; - }, - onSaved: (val) => session.timeStart = stringToTime(val), - ), - TextFormField( - decoration: const InputDecoration(labelText: 'End Time'), - controller: timeEndController, - onTap: () async { - FocusScope.of(ctx).requestFocus(FocusNode()); - final picked = await showTimePicker( - context: ctx, - initialTime: session.timeEnd ?? TimeOfDay.now(), - ); - if (picked != null) { - timeEndController.text = timeToString(picked)!; - session.timeEnd = picked; - } - }, - onSaved: (val) => session.timeEnd = stringToTime(val), - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () async { - if (_formKey.currentState?.validate() ?? false) { - _formKey.currentState?.save(); - - try { - await routinesProvider.editSession(session); - Navigator.of(ctx).pop(); - } catch (e) { - showErrorDialog(e, context); - } - } - }, - child: const Text('Save'), - ), - ], - ); - }, - ); -} diff --git a/test/workout/forms/session_form_test.dart b/test/workout/forms/session_form_test.dart new file mode 100644 index 00000000..d10af65e --- /dev/null +++ b/test/workout/forms/session_form_test.dart @@ -0,0 +1,130 @@ +// test/widgets/routines/forms/session_form_test.dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/workouts/session.dart'; +import 'package:wger/providers/routines.dart'; +import 'package:wger/widgets/routines/forms/session.dart'; + +import 'session_form_test.mocks.dart'; + +@GenerateMocks([RoutinesProvider]) +void main() { + late MockRoutinesProvider mockRoutinesProvider; + + setUp(() { + mockRoutinesProvider = MockRoutinesProvider(); + }); + + Future pumpSessionForm( + WidgetTester tester, { + WorkoutSession? session, + int routineId = 1, + Function()? onSaved, + }) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: mockRoutinesProvider, + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SessionForm( + routineId, + session: session, + onSaved: onSaved, + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + } + + group('SessionForm', () { + testWidgets('renders correctly for an existing session', (WidgetTester tester) async { + //Arrange + final existingSession = WorkoutSession( + id: 1, + routineId: 1, + notes: 'Existing notes', + impression: 1, + date: DateTime.now(), + timeStart: const TimeOfDay(hour: 10, minute: 0), + timeEnd: const TimeOfDay(hour: 11, minute: 0), + ); + + //Act + await pumpSessionForm(tester, session: existingSession); + + //Assert + expect(find.widgetWithText(TextFormField, 'Existing notes'), findsOneWidget); + final toggleButtons = tester.widget(find.byType(ToggleButtons)); + expect(toggleButtons.isSelected, [true, false, false]); // Bad impression + expect(find.widgetWithText(TextFormField, '10:00'), findsOneWidget); + expect(find.widgetWithText(TextFormField, '11:00'), findsOneWidget); + }); + + testWidgets('saves a new session', (WidgetTester tester) async { + // Arrange + bool onSavedCalled = false; + await pumpSessionForm(tester, onSaved: () => onSavedCalled = true); + + when(mockRoutinesProvider.addSession(any, any)).thenAnswer( + (_) async => WorkoutSession(id: 1, routineId: 1, date: DateTime.now()), + ); + + // Act + await tester.enterText(find.widgetWithText(TextFormField, 'Notes'), 'New session notes'); + await tester.tap(find.byKey(const ValueKey('save-button'))); + await tester.pumpAndSettle(); + + // Assert + verify(mockRoutinesProvider.addSession(any, 1)).called(1); + expect(onSavedCalled, isTrue); + }); + + testWidgets('saves an existing session', (WidgetTester tester) async { + // Arrange + bool onSavedCalled = false; + final existingSession = WorkoutSession( + id: 1, + routineId: 1, + notes: 'Old notes', + impression: 2, + date: DateTime.now(), + ); + when(mockRoutinesProvider.editSession(any)).thenAnswer( + (_) async => WorkoutSession( + id: 1, + routineId: 1, + date: DateTime.now(), + ), + ); + + // Act + await pumpSessionForm( + tester, + session: existingSession, + onSaved: () => onSavedCalled = true, + ); + await tester.enterText(find.widgetWithText(TextFormField, 'Old notes'), 'Updated notes'); + await tester.tap(find.byKey(const ValueKey('save-button'))); + await tester.pumpAndSettle(); + + // Assert + final captured = + verify(mockRoutinesProvider.editSession(captureAny)).captured.single as WorkoutSession; + expect(captured.notes, 'Updated notes'); + expect(onSavedCalled, isTrue); + }); + }); +} diff --git a/test/workout/forms/session_form_test.mocks.dart b/test/workout/forms/session_form_test.mocks.dart new file mode 100644 index 00000000..123ec093 --- /dev/null +++ b/test/workout/forms/session_form_test.mocks.dart @@ -0,0 +1,827 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/workout/forms/session_form_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i13; +import 'dart:ui' as _i16; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i15; +import 'package:wger/models/workouts/base_config.dart' as _i9; +import 'package:wger/models/workouts/day.dart' as _i6; +import 'package:wger/models/workouts/day_data.dart' as _i14; +import 'package:wger/models/workouts/log.dart' as _i11; +import 'package:wger/models/workouts/repetition_unit.dart' as _i4; +import 'package:wger/models/workouts/routine.dart' as _i5; +import 'package:wger/models/workouts/session.dart' as _i10; +import 'package:wger/models/workouts/slot.dart' as _i7; +import 'package:wger/models/workouts/slot_entry.dart' as _i8; +import 'package:wger/models/workouts/weight_unit.dart' as _i3; +import 'package:wger/providers/base_provider.dart' as _i2; +import 'package:wger/providers/routines.dart' as _i12; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWeightUnit_1 extends _i1.SmartFake implements _i3.WeightUnit { + _FakeWeightUnit_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRepetitionUnit_2 extends _i1.SmartFake implements _i4.RepetitionUnit { + _FakeRepetitionUnit_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRoutine_3 extends _i1.SmartFake implements _i5.Routine { + _FakeRoutine_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDay_4 extends _i1.SmartFake implements _i6.Day { + _FakeDay_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSlot_5 extends _i1.SmartFake implements _i7.Slot { + _FakeSlot_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSlotEntry_6 extends _i1.SmartFake implements _i8.SlotEntry { + _FakeSlotEntry_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBaseConfig_7 extends _i1.SmartFake implements _i9.BaseConfig { + _FakeBaseConfig_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWorkoutSession_8 extends _i1.SmartFake implements _i10.WorkoutSession { + _FakeWorkoutSession_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeLog_9 extends _i1.SmartFake implements _i11.Log { + _FakeLog_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [RoutinesProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider { + MockRoutinesProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) as _i2.WgerBaseProvider); + + @override + List<_i5.Routine> get items => (super.noSuchMethod( + Invocation.getter(#items), + returnValue: <_i5.Routine>[], + ) as List<_i5.Routine>); + + @override + List<_i3.WeightUnit> get weightUnits => (super.noSuchMethod( + Invocation.getter(#weightUnits), + returnValue: <_i3.WeightUnit>[], + ) as List<_i3.WeightUnit>); + + @override + _i3.WeightUnit get defaultWeightUnit => (super.noSuchMethod( + Invocation.getter(#defaultWeightUnit), + returnValue: _FakeWeightUnit_1( + this, + Invocation.getter(#defaultWeightUnit), + ), + ) as _i3.WeightUnit); + + @override + List<_i4.RepetitionUnit> get repetitionUnits => (super.noSuchMethod( + Invocation.getter(#repetitionUnits), + returnValue: <_i4.RepetitionUnit>[], + ) as List<_i4.RepetitionUnit>); + + @override + _i4.RepetitionUnit get defaultRepetitionUnit => (super.noSuchMethod( + Invocation.getter(#defaultRepetitionUnit), + returnValue: _FakeRepetitionUnit_2( + this, + Invocation.getter(#defaultRepetitionUnit), + ), + ) as _i4.RepetitionUnit); + + @override + set activeRoutine(_i5.Routine? _activeRoutine) => super.noSuchMethod( + Invocation.setter( + #activeRoutine, + _activeRoutine, + ), + returnValueForMissingStub: null, + ); + + @override + set weightUnits(List<_i3.WeightUnit>? weightUnits) => super.noSuchMethod( + Invocation.setter( + #weightUnits, + weightUnits, + ), + returnValueForMissingStub: null, + ); + + @override + set repetitionUnits(List<_i4.RepetitionUnit>? repetitionUnits) => super.noSuchMethod( + Invocation.setter( + #repetitionUnits, + repetitionUnits, + ), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i3.WeightUnit findWeightUnitById(int? id) => (super.noSuchMethod( + Invocation.method( + #findWeightUnitById, + [id], + ), + returnValue: _FakeWeightUnit_1( + this, + Invocation.method( + #findWeightUnitById, + [id], + ), + ), + ) as _i3.WeightUnit); + + @override + _i4.RepetitionUnit findRepetitionUnitById(int? id) => (super.noSuchMethod( + Invocation.method( + #findRepetitionUnitById, + [id], + ), + returnValue: _FakeRepetitionUnit_2( + this, + Invocation.method( + #findRepetitionUnitById, + [id], + ), + ), + ) as _i4.RepetitionUnit); + + @override + List<_i5.Routine> getPlans() => (super.noSuchMethod( + Invocation.method( + #getPlans, + [], + ), + returnValue: <_i5.Routine>[], + ) as List<_i5.Routine>); + + @override + _i5.Routine findById(int? id) => (super.noSuchMethod( + Invocation.method( + #findById, + [id], + ), + returnValue: _FakeRoutine_3( + this, + Invocation.method( + #findById, + [id], + ), + ), + ) as _i5.Routine); + + @override + int findIndexById(int? id) => (super.noSuchMethod( + Invocation.method( + #findIndexById, + [id], + ), + returnValue: 0, + ) as int); + + @override + void setActiveRoutine() => super.noSuchMethod( + Invocation.method( + #setActiveRoutine, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i13.Future fetchAndSetAllRoutinesFull() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetAllRoutinesFull, + [], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future fetchAndSetAllRoutinesSparse() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetAllRoutinesSparse, + [], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future setExercisesAndUnits(List<_i14.DayData>? entries) => (super.noSuchMethod( + Invocation.method( + #setExercisesAndUnits, + [entries], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future<_i5.Routine> fetchAndSetRoutineSparse(int? planId) => (super.noSuchMethod( + Invocation.method( + #fetchAndSetRoutineSparse, + [planId], + ), + returnValue: _i13.Future<_i5.Routine>.value(_FakeRoutine_3( + this, + Invocation.method( + #fetchAndSetRoutineSparse, + [planId], + ), + )), + ) as _i13.Future<_i5.Routine>); + + @override + _i13.Future<_i5.Routine> fetchAndSetRoutineFull(int? routineId) => (super.noSuchMethod( + Invocation.method( + #fetchAndSetRoutineFull, + [routineId], + ), + returnValue: _i13.Future<_i5.Routine>.value(_FakeRoutine_3( + this, + Invocation.method( + #fetchAndSetRoutineFull, + [routineId], + ), + )), + ) as _i13.Future<_i5.Routine>); + + @override + _i13.Future<_i5.Routine> addRoutine(_i5.Routine? routine) => (super.noSuchMethod( + Invocation.method( + #addRoutine, + [routine], + ), + returnValue: _i13.Future<_i5.Routine>.value(_FakeRoutine_3( + this, + Invocation.method( + #addRoutine, + [routine], + ), + )), + ) as _i13.Future<_i5.Routine>); + + @override + _i13.Future editRoutine(_i5.Routine? routine) => (super.noSuchMethod( + Invocation.method( + #editRoutine, + [routine], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future deleteRoutine(int? id) => (super.noSuchMethod( + Invocation.method( + #deleteRoutine, + [id], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future fetchAndSetRepetitionUnits() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetRepetitionUnits, + [], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future fetchAndSetWeightUnits() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetWeightUnits, + [], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future fetchAndSetUnits() => (super.noSuchMethod( + Invocation.method( + #fetchAndSetUnits, + [], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future<_i6.Day> addDay(_i6.Day? day) => (super.noSuchMethod( + Invocation.method( + #addDay, + [day], + ), + returnValue: _i13.Future<_i6.Day>.value(_FakeDay_4( + this, + Invocation.method( + #addDay, + [day], + ), + )), + ) as _i13.Future<_i6.Day>); + + @override + _i13.Future editDay(_i6.Day? day) => (super.noSuchMethod( + Invocation.method( + #editDay, + [day], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future editDays(List<_i6.Day>? days) => (super.noSuchMethod( + Invocation.method( + #editDays, + [days], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future deleteDay(int? dayId) => (super.noSuchMethod( + Invocation.method( + #deleteDay, + [dayId], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future<_i7.Slot> addSlot( + _i7.Slot? slot, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #addSlot, + [ + slot, + routineId, + ], + ), + returnValue: _i13.Future<_i7.Slot>.value(_FakeSlot_5( + this, + Invocation.method( + #addSlot, + [ + slot, + routineId, + ], + ), + )), + ) as _i13.Future<_i7.Slot>); + + @override + _i13.Future deleteSlot( + int? slotId, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteSlot, + [ + slotId, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future editSlot( + _i7.Slot? slot, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #editSlot, + [ + slot, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future editSlots( + List<_i7.Slot>? slots, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #editSlots, + [ + slots, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future<_i8.SlotEntry> addSlotEntry( + _i8.SlotEntry? entry, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #addSlotEntry, + [ + entry, + routineId, + ], + ), + returnValue: _i13.Future<_i8.SlotEntry>.value(_FakeSlotEntry_6( + this, + Invocation.method( + #addSlotEntry, + [ + entry, + routineId, + ], + ), + )), + ) as _i13.Future<_i8.SlotEntry>); + + @override + _i13.Future deleteSlotEntry( + int? id, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteSlotEntry, + [ + id, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future editSlotEntry( + _i8.SlotEntry? entry, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #editSlotEntry, + [ + entry, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + String getConfigUrl(_i8.ConfigType? type) => (super.noSuchMethod( + Invocation.method( + #getConfigUrl, + [type], + ), + returnValue: _i15.dummyValue( + this, + Invocation.method( + #getConfigUrl, + [type], + ), + ), + ) as String); + + @override + _i13.Future<_i9.BaseConfig> editConfig( + _i9.BaseConfig? config, + _i8.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method( + #editConfig, + [ + config, + type, + ], + ), + returnValue: _i13.Future<_i9.BaseConfig>.value(_FakeBaseConfig_7( + this, + Invocation.method( + #editConfig, + [ + config, + type, + ], + ), + )), + ) as _i13.Future<_i9.BaseConfig>); + + @override + _i13.Future<_i9.BaseConfig> addConfig( + _i9.BaseConfig? config, + _i8.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method( + #addConfig, + [ + config, + type, + ], + ), + returnValue: _i13.Future<_i9.BaseConfig>.value(_FakeBaseConfig_7( + this, + Invocation.method( + #addConfig, + [ + config, + type, + ], + ), + )), + ) as _i13.Future<_i9.BaseConfig>); + + @override + _i13.Future deleteConfig( + int? id, + _i8.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteConfig, + [ + id, + type, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future handleConfig( + _i8.SlotEntry? entry, + String? input, + _i8.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method( + #handleConfig, + [ + entry, + input, + type, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + _i13.Future> fetchSessionData() => (super.noSuchMethod( + Invocation.method( + #fetchSessionData, + [], + ), + returnValue: _i13.Future>.value(<_i10.WorkoutSession>[]), + ) as _i13.Future>); + + @override + _i13.Future<_i10.WorkoutSession> addSession( + _i10.WorkoutSession? session, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #addSession, + [ + session, + routineId, + ], + ), + returnValue: _i13.Future<_i10.WorkoutSession>.value(_FakeWorkoutSession_8( + this, + Invocation.method( + #addSession, + [ + session, + routineId, + ], + ), + )), + ) as _i13.Future<_i10.WorkoutSession>); + + @override + _i13.Future<_i10.WorkoutSession> editSession(_i10.WorkoutSession? session) => (super.noSuchMethod( + Invocation.method( + #editSession, + [session], + ), + returnValue: _i13.Future<_i10.WorkoutSession>.value(_FakeWorkoutSession_8( + this, + Invocation.method( + #editSession, + [session], + ), + )), + ) as _i13.Future<_i10.WorkoutSession>); + + @override + _i13.Future<_i11.Log> addLog(_i11.Log? log) => (super.noSuchMethod( + Invocation.method( + #addLog, + [log], + ), + returnValue: _i13.Future<_i11.Log>.value(_FakeLog_9( + this, + Invocation.method( + #addLog, + [log], + ), + )), + ) as _i13.Future<_i11.Log>); + + @override + _i13.Future deleteLog( + int? logId, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteLog, + [ + logId, + routineId, + ], + ), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), + ) as _i13.Future); + + @override + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +}