Move common logic to SessionForm

Refactor and polish a bit the form in the log overview.
This commit is contained in:
Roland Geider
2025-05-09 21:12:49 +02:00
parent e8ff3458e0
commit d663bcd88a
8 changed files with 1288 additions and 367 deletions

View File

@@ -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 = '',

View File

@@ -9,10 +9,11 @@ part of 'session.dart';
WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> 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<String, dynamic> json) {
Map<String, dynamic> _$WorkoutSessionToJson(WorkoutSession instance) => <String, dynamic>{
'id': instance.id,
'routine': instance.routineId,
'day': instance.dayId,
'date': dateToYYYYMMDD(instance.date),
'impression': numToString(instance.impression),
'notes': instance.notes,

View File

@@ -0,0 +1,240 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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<SessionForm> {
final _form = GlobalKey<FormState>();
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<RoutinesProvider>();
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);
}
}
},
),
],
),
);
}
}

View File

@@ -194,6 +194,7 @@ class _GymModeState extends ConsumerState<GymMode> {
_controller,
ref.read(gymStateProvider).startTime,
_exercisePages,
dayId: widget._dayDataGym.day!.id!,
),
];

View File

@@ -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<Exercise, int> _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<SessionPage> {
final _form = GlobalKey<FormState>();
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<RoutinesProvider>();
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),
],
);
}

View File

@@ -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<SessionInfo> createState() => _SessionInfoState();
}
class _SessionInfoState extends State<SessionInfo> {
bool editMode = false;
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
return GestureDetector(
onTap: (){
final routinesProvider = context.read<RoutinesProvider>();
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<FormState>();
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<bool> 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'),
),
],
);
},
);
}

View File

@@ -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<void> pumpSessionForm(
WidgetTester tester, {
WorkoutSession? session,
int routineId = 1,
Function()? onSaved,
}) async {
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<RoutinesProvider>.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<ToggleButtons>(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);
});
});
}

View File

@@ -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<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllRoutinesFull,
[],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchAndSetAllRoutinesSparse() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllRoutinesSparse,
[],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> setExercisesAndUnits(List<_i14.DayData>? entries) => (super.noSuchMethod(
Invocation.method(
#setExercisesAndUnits,
[entries],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@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<void> editRoutine(_i5.Routine? routine) => (super.noSuchMethod(
Invocation.method(
#editRoutine,
[routine],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> deleteRoutine(int? id) => (super.noSuchMethod(
Invocation.method(
#deleteRoutine,
[id],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchAndSetRepetitionUnits() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetRepetitionUnits,
[],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchAndSetWeightUnits() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetWeightUnits,
[],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchAndSetUnits() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetUnits,
[],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@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<void> editDay(_i6.Day? day) => (super.noSuchMethod(
Invocation.method(
#editDay,
[day],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editDays(List<_i6.Day>? days) => (super.noSuchMethod(
Invocation.method(
#editDays,
[days],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> deleteDay(int? dayId) => (super.noSuchMethod(
Invocation.method(
#deleteDay,
[dayId],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@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<void> deleteSlot(
int? slotId,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#deleteSlot,
[
slotId,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlot(
_i7.Slot? slot,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#editSlot,
[
slot,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlots(
List<_i7.Slot>? slots,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#editSlots,
[
slots,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@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<void> deleteSlotEntry(
int? id,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#deleteSlotEntry,
[
id,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlotEntry(
_i8.SlotEntry? entry,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#editSlotEntry,
[
entry,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
String getConfigUrl(_i8.ConfigType? type) => (super.noSuchMethod(
Invocation.method(
#getConfigUrl,
[type],
),
returnValue: _i15.dummyValue<String>(
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<void> deleteConfig(
int? id,
_i8.ConfigType? type,
) =>
(super.noSuchMethod(
Invocation.method(
#deleteConfig,
[
id,
type,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> handleConfig(
_i8.SlotEntry? entry,
String? input,
_i8.ConfigType? type,
) =>
(super.noSuchMethod(
Invocation.method(
#handleConfig,
[
entry,
input,
type,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<List<_i10.WorkoutSession>> fetchSessionData() => (super.noSuchMethod(
Invocation.method(
#fetchSessionData,
[],
),
returnValue: _i13.Future<List<_i10.WorkoutSession>>.value(<_i10.WorkoutSession>[]),
) as _i13.Future<List<_i10.WorkoutSession>>);
@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<void> deleteLog(
int? logId,
int? routineId,
) =>
(super.noSuchMethod(
Invocation.method(
#deleteLog,
[
logId,
routineId,
],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@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,
);
}