Fusionne feature/bugsprint-score-zero-reps-display — analyze propre (mêmes 8 infos préexistantes), 79/79 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#41"
|
issueRef: "#41"
|
||||||
version: 5
|
version: 6
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784379589805
|
updatedAt: 1784406566969
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "d92c2af3-1981-4782-9aba-460a45b37d56"
|
id: "d92c2af3-1981-4782-9aba-460a45b37d56"
|
||||||
number: 41
|
number: 41
|
||||||
title: "[bug] images non affichée dans le caroussel"
|
title: "[bug] images non affichée dans le caroussel"
|
||||||
status: "inProgress"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
||||||
links: []
|
links: []
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
|
|||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784378925702
|
createdAt: 1784378925702
|
||||||
updatedAt: 1784379589805
|
updatedAt: 1784406566969
|
||||||
version: 5
|
version: 6
|
||||||
---
|
---
|
||||||
Dans le carroussel des exercices dans les séances, les images ne s'affichent pas vraiment, je n'ai qu'une icone avec le titre image 1, image 2 etc...
|
Dans le carroussel des exercices dans les séances, les images ne s'affichent pas vraiment, je n'ai qu'une icone avec le titre image 1, image 2 etc...
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#42"
|
issueRef: "#42"
|
||||||
version: 5
|
version: 6
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784379590525
|
updatedAt: 1784406567477
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "a788671c-fbc4-4c3d-855e-bb44c120ab54"
|
id: "a788671c-fbc4-4c3d-855e-bb44c120ab54"
|
||||||
number: 42
|
number: 42
|
||||||
title: "[bug] vidéo non affichée dans le caroussel"
|
title: "[bug] vidéo non affichée dans le caroussel"
|
||||||
status: "inProgress"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
||||||
links: []
|
links: []
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
|
|||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784378998844
|
createdAt: 1784378998844
|
||||||
updatedAt: 1784379590525
|
updatedAt: 1784406567477
|
||||||
version: 5
|
version: 6
|
||||||
---
|
---
|
||||||
Dans les exerccies des séances, je ne peux pas lancer la vidéo. Je n'ai qu'un icone Play avec marqué Vidéo mais rien ne se passe si je selectionne
|
Dans les exerccies des séances, je ne peux pas lancer la vidéo. Je n'ai qu'un icone Play avec marqué Vidéo mais rien ne se passe si je selectionne
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#43"
|
issueRef: "#43"
|
||||||
version: 5
|
version: 6
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784379591252
|
updatedAt: 1784406567925
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "33546028-43ab-467c-90af-f2ff3aeb920f"
|
id: "33546028-43ab-467c-90af-f2ff3aeb920f"
|
||||||
number: 43
|
number: 43
|
||||||
title: "[Bug] chrono qui n'affiche que les secondes"
|
title: "[Bug] chrono qui n'affiche que les secondes"
|
||||||
status: "inProgress"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
||||||
links: []
|
links: []
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
|
|||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784379078126
|
createdAt: 1784379078126
|
||||||
updatedAt: 1784379591252
|
updatedAt: 1784406567925
|
||||||
version: 5
|
version: 6
|
||||||
---
|
---
|
||||||
Pour le chrono score, quand je le lance j'ai d'abord 0.9 sec qui s'affiche, puis uniquement les secondes qui s'incrémentent
|
Pour le chrono score, quand je le lance j'ai d'abord 0.9 sec qui s'affiche, puis uniquement les secondes qui s'incrémentent
|
||||||
@ -465,37 +465,37 @@
|
|||||||
"issueRef": "#41",
|
"issueRef": "#41",
|
||||||
"path": "41",
|
"path": "41",
|
||||||
"title": "[bug] images non affichée dans le caroussel",
|
"title": "[bug] images non affichée dans le caroussel",
|
||||||
"status": "inProgress",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784379589805
|
"updatedAt": 1784406566969
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#42",
|
"issueRef": "#42",
|
||||||
"path": "42",
|
"path": "42",
|
||||||
"title": "[bug] vidéo non affichée dans le caroussel",
|
"title": "[bug] vidéo non affichée dans le caroussel",
|
||||||
"status": "inProgress",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784379590525
|
"updatedAt": 1784406567477
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#43",
|
"issueRef": "#43",
|
||||||
"path": "43",
|
"path": "43",
|
||||||
"title": "[Bug] chrono qui n'affiche que les secondes",
|
"title": "[Bug] chrono qui n'affiche que les secondes",
|
||||||
"status": "inProgress",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784379591252
|
"updatedAt": 1784406567925
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#44",
|
"issueRef": "#44",
|
||||||
|
|||||||
@ -1646,8 +1646,8 @@ void _validateExerciseDefaultTargets({
|
|||||||
}
|
}
|
||||||
if (hasScoreMeasure &&
|
if (hasScoreMeasure &&
|
||||||
scoreInputMode == ScoreInputMode.manual &&
|
scoreInputMode == ScoreInputMode.manual &&
|
||||||
(defaultTargetScore == null || defaultTargetScore <= 0)) {
|
(defaultTargetScore == null || defaultTargetScore < 0)) {
|
||||||
throw const DomainException('Default target score must be positive.');
|
throw const DomainException('Default target score must not be negative.');
|
||||||
}
|
}
|
||||||
if (defaultTargetScore != null &&
|
if (defaultTargetScore != null &&
|
||||||
(!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) {
|
(!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) {
|
||||||
|
|||||||
@ -167,7 +167,10 @@ final class Exercise {
|
|||||||
'Default target time seconds',
|
'Default target time seconds',
|
||||||
);
|
);
|
||||||
_requireNullablePositive(defaultTargetReps, 'Default target reps');
|
_requireNullablePositive(defaultTargetReps, 'Default target reps');
|
||||||
_requireNullablePositiveDouble(defaultTargetScore, 'Default target score');
|
_requireNullableNonNegativeDouble(
|
||||||
|
defaultTargetScore,
|
||||||
|
'Default target score',
|
||||||
|
);
|
||||||
_requireNullablePositive(
|
_requireNullablePositive(
|
||||||
defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs,
|
||||||
'Default target score time ms',
|
'Default target score time ms',
|
||||||
@ -1035,12 +1038,6 @@ void _requireNullableNonNegativeDouble(double? value, String label) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _requireNullablePositiveDouble(double? value, String label) {
|
|
||||||
if (value != null && value <= 0) {
|
|
||||||
throw DomainException('$label must be positive.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _requireScoreTargetShape({
|
void _requireScoreTargetShape({
|
||||||
required bool scoreEnabled,
|
required bool scoreEnabled,
|
||||||
required ScoreInputMode scoreInputMode,
|
required ScoreInputMode scoreInputMode,
|
||||||
|
|||||||
@ -635,9 +635,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_scoreInputMode != ScoreInputMode.manual) {
|
_scoreInputMode != ScoreInputMode.manual) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return _positiveDoubleValidator(
|
return _nonNegativeDoubleValidator(
|
||||||
value,
|
value,
|
||||||
'Saisis un score supérieur à 0.',
|
'Saisis un score positif ou nul.',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -914,6 +914,14 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _nonNegativeDoubleValidator(String? value, String message) {
|
||||||
|
final number = double.tryParse(value?.trim() ?? '');
|
||||||
|
if (number == null || number < 0) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String _optionalIntText(int? value) {
|
String _optionalIntText(int? value) {
|
||||||
return value == null ? '' : '$value';
|
return value == null ? '' : '$value';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_mode = _session.status == ActiveWorkoutStatus.running
|
_mode = _session.status == ActiveWorkoutStatus.running
|
||||||
? WorkoutExecutionMode.active
|
? WorkoutExecutionMode.active
|
||||||
: WorkoutExecutionMode.paused;
|
: WorkoutExecutionMode.paused;
|
||||||
|
_reps = _initialRepsFor(_exercise);
|
||||||
_seriesStartedAt = DateTime.now().toUtc();
|
_seriesStartedAt = DateTime.now().toUtc();
|
||||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
@ -808,7 +809,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
|
|
||||||
Future<void> _moveTo(ExecutionPosition position) async {
|
Future<void> _moveTo(ExecutionPosition position) async {
|
||||||
_scoreController.clear();
|
_scoreController.clear();
|
||||||
_reps = 0;
|
_reps = _initialRepsFor(_plan.exerciseAt(position));
|
||||||
_activeRestStateId = null;
|
_activeRestStateId = null;
|
||||||
_scoreStopwatch = null;
|
_scoreStopwatch = null;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
@ -889,6 +890,11 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _initialRepsFor(ExecutionExercise exercise) {
|
||||||
|
if (!exercise.repsEnabled) return 0;
|
||||||
|
return exercise.targetReps ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SetMeasureInput extends StatelessWidget {
|
final class SetMeasureInput extends StatelessWidget {
|
||||||
|
|||||||
@ -55,7 +55,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Exercise use case rejects non-positive default targets', () async {
|
test('Exercise use case rejects invalid default targets', () async {
|
||||||
final useCase = _exerciseUseCase(_FakeExerciseRepository());
|
final useCase = _exerciseUseCase(_FakeExerciseRepository());
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
@ -86,12 +86,33 @@ void main() {
|
|||||||
hasScoreMeasure: true,
|
hasScoreMeasure: true,
|
||||||
scoreLabel: 'Charge',
|
scoreLabel: 'Charge',
|
||||||
scoreUnit: 'kg',
|
scoreUnit: 'kg',
|
||||||
defaultTargetScore: 0,
|
defaultTargetScore: -1,
|
||||||
),
|
),
|
||||||
throwsA(isA<DomainException>()),
|
throwsA(isA<DomainException>()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Exercise use case accepts a zero manual score default target',
|
||||||
|
() async {
|
||||||
|
final repository = _FakeExerciseRepository();
|
||||||
|
final useCase = _exerciseUseCase(repository);
|
||||||
|
|
||||||
|
final exercise = await useCase.create(
|
||||||
|
name: 'Charge',
|
||||||
|
hasTimeMeasure: false,
|
||||||
|
hasRepsMeasure: false,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Charge',
|
||||||
|
scoreUnit: 'kg',
|
||||||
|
defaultTargetScore: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exercise.defaultTargetScore, 0);
|
||||||
|
expect(repository.saved.single.defaultTargetScore, 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Exercise use case accepts stopwatch score without default target',
|
'Exercise use case accepts stopwatch score without default target',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
@ -43,6 +43,42 @@ void main() {
|
|||||||
expect(find.text('00:45'), findsOneWidget);
|
expect(find.text('00:45'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('les répétitions sont initialisées avec la cible', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
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: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(targetReps: 8),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(
|
||||||
|
_FakeActiveSessionRepository(),
|
||||||
|
clock,
|
||||||
|
),
|
||||||
|
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Répétitions'), findsOneWidget);
|
||||||
|
expect(find.text('8'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
|
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@ -618,8 +654,8 @@ void main() {
|
|||||||
expect(find.text('2 / 3'), findsOneWidget);
|
expect(find.text('2 / 3'), findsOneWidget);
|
||||||
expect(find.text('30 s'), findsOneWidget);
|
expect(find.text('30 s'), findsOneWidget);
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++) {
|
for (var i = 0; i < 2; i++) {
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
await tester.tap(find.byIcon(Icons.remove));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
}
|
}
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
@ -637,6 +673,7 @@ void main() {
|
|||||||
expect(repository.session?.currentSetIndex, 2);
|
expect(repository.session?.currentSetIndex, 2);
|
||||||
expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget);
|
expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget);
|
||||||
expect(find.text('3 / 3'), findsOneWidget);
|
expect(find.text('3 / 3'), findsOneWidget);
|
||||||
|
expect(find.text('5'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user