feat(ui): édition ponctuelle des séries en séance (ticket #23)
Expose l'édition ponctuelle des séries (backend du ticket #21) sur workout_execution_screen.dart, sans impacter le modèle de séance. flutter analyze propre, 33/33 tests verts, build APK debug validé. Clôture le lot navigation libre / programme simplifié (tickets #20 à #23). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -323,10 +323,12 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
builder: (context) => FractionallySizedBox(
|
builder: (context) => FractionallySizedBox(
|
||||||
heightFactor: 1,
|
heightFactor: 1,
|
||||||
child: _WorkoutPlanSheet(
|
child: _WorkoutPlanSheet(
|
||||||
|
sessionId: _session.metadata.id,
|
||||||
plan: _plan,
|
plan: _plan,
|
||||||
currentPosition: _position,
|
currentPosition: _position,
|
||||||
setStates: setStates,
|
setStates: setStates,
|
||||||
activeRest: activeRest,
|
activeRest: activeRest,
|
||||||
|
activeUseCases: widget.activeUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -609,15 +611,19 @@ final class SetMeasureInput extends StatelessWidget {
|
|||||||
|
|
||||||
final class _WorkoutPlanSheet extends StatefulWidget {
|
final class _WorkoutPlanSheet extends StatefulWidget {
|
||||||
const _WorkoutPlanSheet({
|
const _WorkoutPlanSheet({
|
||||||
|
required this.sessionId,
|
||||||
required this.plan,
|
required this.plan,
|
||||||
required this.currentPosition,
|
required this.currentPosition,
|
||||||
required this.setStates,
|
required this.setStates,
|
||||||
|
required this.activeUseCases,
|
||||||
this.activeRest,
|
this.activeRest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String sessionId;
|
||||||
final WorkoutExecutionPlan plan;
|
final WorkoutExecutionPlan plan;
|
||||||
final ExecutionPosition currentPosition;
|
final ExecutionPosition currentPosition;
|
||||||
final List<SetResultPositionState> setStates;
|
final List<SetResultPositionState> setStates;
|
||||||
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveRestState? activeRest;
|
final ActiveRestState? activeRest;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -627,10 +633,12 @@ final class _WorkoutPlanSheet extends StatefulWidget {
|
|||||||
final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
late int _remainingRestSeconds;
|
late int _remainingRestSeconds;
|
||||||
|
late List<SetResultPositionState> _setStates;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_setStates = widget.setStates;
|
||||||
_remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
|
_remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
|
||||||
if (widget.activeRest != null && _remainingRestSeconds > 0) {
|
if (widget.activeRest != null && _remainingRestSeconds > 0) {
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
@ -653,7 +661,7 @@ final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final statesByPosition = {
|
final statesByPosition = {
|
||||||
for (final state in widget.setStates) _stateKey(state): state,
|
for (final state in _setStates) _stateKey(state): state,
|
||||||
};
|
};
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@ -715,12 +723,20 @@ final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
|||||||
setIndex: setIndex,
|
setIndex: setIndex,
|
||||||
),
|
),
|
||||||
currentPosition: widget.currentPosition,
|
currentPosition: widget.currentPosition,
|
||||||
|
exercise: widget
|
||||||
|
.plan
|
||||||
|
.programs[programIndex]
|
||||||
|
.exercises[exerciseIndex],
|
||||||
state:
|
state:
|
||||||
statesByPosition[_positionKey(
|
statesByPosition[_positionKey(
|
||||||
programIndex,
|
programIndex,
|
||||||
exerciseIndex,
|
exerciseIndex,
|
||||||
setIndex,
|
setIndex,
|
||||||
)],
|
)],
|
||||||
|
onUpdated: _refreshAfterEdit,
|
||||||
|
activeUseCases: widget.activeUseCases,
|
||||||
|
sessionId: widget.sessionId,
|
||||||
|
activeRest: widget.activeRest,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
@ -729,6 +745,17 @@ final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshAfterEdit() async {
|
||||||
|
final updated = await widget.activeUseCases.listSetResults(
|
||||||
|
widget.sessionId,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _setStates = updated);
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Série mise à jour')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _RestPlanBanner extends StatelessWidget {
|
final class _RestPlanBanner extends StatelessWidget {
|
||||||
@ -752,17 +779,30 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
const _WorkoutPlanSetTile({
|
const _WorkoutPlanSetTile({
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.currentPosition,
|
required this.currentPosition,
|
||||||
|
required this.exercise,
|
||||||
|
required this.onUpdated,
|
||||||
|
required this.activeUseCases,
|
||||||
|
required this.sessionId,
|
||||||
|
this.activeRest,
|
||||||
this.state,
|
this.state,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ExecutionPosition position;
|
final ExecutionPosition position;
|
||||||
final ExecutionPosition currentPosition;
|
final ExecutionPosition currentPosition;
|
||||||
|
final ExecutionExercise exercise;
|
||||||
final SetResultPositionState? state;
|
final SetResultPositionState? state;
|
||||||
|
final Future<void> Function() onUpdated;
|
||||||
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final String sessionId;
|
||||||
|
final ActiveRestState? activeRest;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final planState = _planSetState(position, currentPosition, state);
|
final planState = _planSetState(position, currentPosition, state);
|
||||||
final tappable = planState != _PlanSetStatus.todo;
|
final tappable = planState != _PlanSetStatus.todo;
|
||||||
|
final VoidCallback? onTap = tappable
|
||||||
|
? () => unawaited(_handleTap(context, planState))
|
||||||
|
: null;
|
||||||
final tokens = courtBlazerTokensOf(context);
|
final tokens = courtBlazerTokensOf(context);
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
@ -778,11 +818,273 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
enabled: tappable,
|
enabled: tappable,
|
||||||
title: Text('Série ${position.setIndex + 1}'),
|
title: Text('Série ${position.setIndex + 1}'),
|
||||||
subtitle: Text(_planSetSubtitle(planState, state?.result)),
|
subtitle: Text(_planSetSubtitle(planState, state?.result)),
|
||||||
trailing: _PlanStatusBadge(
|
trailing: _PlanStatusBadge(status: planState, onTap: onTap),
|
||||||
status: planState,
|
onTap: onTap,
|
||||||
onTap: tappable ? () => Navigator.of(context).pop() : null,
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTap(BuildContext context, _PlanSetStatus status) async {
|
||||||
|
if (status == _PlanSetStatus.current) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (context) => _EditSetResultSheet(
|
||||||
|
sessionId: sessionId,
|
||||||
|
position: position,
|
||||||
|
exercise: exercise,
|
||||||
|
state: state,
|
||||||
|
status: status,
|
||||||
|
activeUseCases: activeUseCases,
|
||||||
|
activeRest: activeRest,
|
||||||
|
onSaved: onUpdated,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _EditSetResultSheet extends StatefulWidget {
|
||||||
|
const _EditSetResultSheet({
|
||||||
|
required this.sessionId,
|
||||||
|
required this.position,
|
||||||
|
required this.exercise,
|
||||||
|
required this.state,
|
||||||
|
required this.status,
|
||||||
|
required this.activeUseCases,
|
||||||
|
required this.onSaved,
|
||||||
|
this.activeRest,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String sessionId;
|
||||||
|
final ExecutionPosition position;
|
||||||
|
final ExecutionExercise exercise;
|
||||||
|
final SetResultPositionState? state;
|
||||||
|
final _PlanSetStatus status;
|
||||||
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final Future<void> Function() onSaved;
|
||||||
|
final ActiveRestState? activeRest;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EditSetResultSheet> createState() => _EditSetResultSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
|
||||||
|
final _timeController = TextEditingController();
|
||||||
|
final _repsController = TextEditingController();
|
||||||
|
final _scoreController = TextEditingController();
|
||||||
|
Timer? _timer;
|
||||||
|
late int _remainingRestSeconds;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final result = widget.state?.result;
|
||||||
|
final actualTimeMs = result?.actualTimeMs;
|
||||||
|
if (actualTimeMs != null) {
|
||||||
|
_timeController.text = (actualTimeMs / 1000).round().toString();
|
||||||
|
}
|
||||||
|
final actualReps = result?.actualReps;
|
||||||
|
if (actualReps != null) {
|
||||||
|
_repsController.text = actualReps.toString();
|
||||||
|
}
|
||||||
|
final actualScore = result?.actualScore;
|
||||||
|
if (actualScore != null) {
|
||||||
|
_scoreController.text = _formatScore(actualScore);
|
||||||
|
}
|
||||||
|
_remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
|
||||||
|
if (widget.activeRest != null && _remainingRestSeconds > 0) {
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_remainingRestSeconds = (_remainingRestSeconds - 1)
|
||||||
|
.clamp(0, 9999)
|
||||||
|
.toInt();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timeController.dispose();
|
||||||
|
_repsController.dispose();
|
||||||
|
_scoreController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isCompleted = widget.status == _PlanSetStatus.completed;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Modifier la série',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Annuler',
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'${widget.exercise.name} · '
|
||||||
|
'Série ${widget.position.setIndex + 1}/${widget.exercise.setsCount}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'État actuel : ${isCompleted ? 'Terminée' : 'Passée'}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
if (widget.activeRest != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_RestPlanBanner(remainingSeconds: _remainingRestSeconds),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (widget.exercise.timeEnabled) ...[
|
||||||
|
TextField(
|
||||||
|
controller: _timeController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Temps réalisé (s)'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
if (widget.exercise.repsEnabled) ...[
|
||||||
|
TextField(
|
||||||
|
controller: _repsController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Répétitions'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
if (widget.exercise.scoreEnabled) ...[
|
||||||
|
TextField(
|
||||||
|
controller: _scoreController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.exercise.scoreUnit == null
|
||||||
|
? 'Score'
|
||||||
|
: 'Score (${widget.exercise.scoreUnit})',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _saveCompleted,
|
||||||
|
child: Text(
|
||||||
|
isCompleted
|
||||||
|
? 'Enregistrer les corrections'
|
||||||
|
: 'Enregistrer le résultat',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _markSkipped,
|
||||||
|
child: const Text('Marquer comme passée'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveCompleted() async {
|
||||||
|
if (!_hasAnyValue()) {
|
||||||
|
if (widget.status == _PlanSetStatus.completed) {
|
||||||
|
final confirmed = await _confirmMarkSkipped();
|
||||||
|
if (confirmed != true) return;
|
||||||
|
}
|
||||||
|
await _saveSkipped();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await widget.activeUseCases.upsertSetResultAtPosition(
|
||||||
|
sessionId: widget.sessionId,
|
||||||
|
programIndex: widget.position.programIndex,
|
||||||
|
exerciseIndex: widget.position.exerciseIndex,
|
||||||
|
setIndex: widget.position.setIndex,
|
||||||
|
status: SetResultStatus.completed,
|
||||||
|
actualTimeMs: widget.exercise.timeEnabled
|
||||||
|
? _secondsToMilliseconds(_timeController.text)
|
||||||
|
: null,
|
||||||
|
actualReps: widget.exercise.repsEnabled
|
||||||
|
? int.tryParse(_repsController.text.trim())
|
||||||
|
: null,
|
||||||
|
actualScore: widget.exercise.scoreEnabled
|
||||||
|
? double.tryParse(_scoreController.text.trim())
|
||||||
|
: null,
|
||||||
|
scoreUnitSnapshot: widget.exercise.scoreUnit,
|
||||||
|
);
|
||||||
|
await _finishSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _markSkipped() async {
|
||||||
|
await _saveSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSkipped() async {
|
||||||
|
await widget.activeUseCases.upsertSetResultAtPosition(
|
||||||
|
sessionId: widget.sessionId,
|
||||||
|
programIndex: widget.position.programIndex,
|
||||||
|
exerciseIndex: widget.position.exerciseIndex,
|
||||||
|
setIndex: widget.position.setIndex,
|
||||||
|
status: SetResultStatus.skipped,
|
||||||
|
);
|
||||||
|
await _finishSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _finishSave() async {
|
||||||
|
await widget.onSaved();
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAnyValue() {
|
||||||
|
return _timeController.text.trim().isNotEmpty ||
|
||||||
|
_repsController.text.trim().isNotEmpty ||
|
||||||
|
_scoreController.text.trim().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _confirmMarkSkipped() {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Supprimer le résultat de cette série ?'),
|
||||||
|
content: const Text(
|
||||||
|
'La série restera dans le plan comme passée, sans résultat saisi.',
|
||||||
),
|
),
|
||||||
onTap: tappable ? () => Navigator.of(context).pop() : null,
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Marquer comme passée'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -872,6 +1174,11 @@ String _formatScore(double value) {
|
|||||||
: value.toString();
|
: value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? _secondsToMilliseconds(String rawValue) {
|
||||||
|
final seconds = int.tryParse(rawValue.trim());
|
||||||
|
return seconds == null ? null : seconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
int _remainingSecondsForRest(ActiveRestState? rest) {
|
int _remainingSecondsForRest(ActiveRestState? rest) {
|
||||||
if (rest == null) return 0;
|
if (rest == null) return 0;
|
||||||
final elapsedSeconds = DateTime.now()
|
final elapsedSeconds = DateTime.now()
|
||||||
|
|||||||
@ -280,6 +280,123 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('Plan de séance'), findsNothing);
|
expect(find.text('Plan de séance'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'modifier une série passée la termine sans changer la position courante',
|
||||||
|
(tester) async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 2,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
setsCount: 4,
|
||||||
|
restSeconds: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Voir le plan'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(ListTile, 'Série 2'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Modifier la série'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.enterText(_textFieldWithLabel('Répétitions'), '12');
|
||||||
|
await tester.ensureVisible(find.text('Enregistrer le résultat'));
|
||||||
|
await tester.tap(find.text('Enregistrer le résultat'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.session?.currentSetIndex, 2);
|
||||||
|
expect(repository.results.single.status, SetResultStatus.completed);
|
||||||
|
expect(repository.results.single.actualReps, 12);
|
||||||
|
expect(find.text('Série mise à jour'), findsWidgets);
|
||||||
|
expect(find.text('12 reps'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Fermer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text('Programme 1/1 · Exercice 1/1 · Série 3/4'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'toucher la série courante dans le plan revient à l’écran principal',
|
||||||
|
(tester) async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 1,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
setsCount: 3,
|
||||||
|
restSeconds: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Voir le plan'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('En cours'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Plan de séance'), findsNothing);
|
||||||
|
expect(find.text('Modifier la série'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.text('Programme 1/1 · Exercice 1/1 · Série 2/3'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder _textFieldWithLabel(String label) {
|
||||||
|
return find.byWidgetPredicate(
|
||||||
|
(widget) => widget is TextField && widget.decoration?.labelText == label,
|
||||||
|
description: 'TextField with label "$label"',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveWorkoutSessionUseCases _activeUseCases(
|
ActiveWorkoutSessionUseCases _activeUseCases(
|
||||||
@ -437,7 +554,18 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||||
results.add(result);
|
final index = results.indexWhere(
|
||||||
|
(saved) =>
|
||||||
|
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||||||
|
saved.programIndex == result.programIndex &&
|
||||||
|
saved.exerciseIndex == result.exerciseIndex &&
|
||||||
|
saved.setIndex == result.setIndex,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
results.add(result);
|
||||||
|
} else {
|
||||||
|
results[index] = result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user