1919import 'package:clock/clock.dart' ;
2020import 'package:flutter/material.dart' ;
2121import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod;
22+ import 'package:flutter_riverpod/misc.dart' as riverpod;
2223import 'package:flutter_test/flutter_test.dart' ;
2324import 'package:mockito/annotations.dart' ;
2425import 'package:mockito/mockito.dart' ;
@@ -35,6 +36,7 @@ import 'package:wger/providers/gym_state.dart';
3536import 'package:wger/providers/network_provider.dart' ;
3637import 'package:wger/providers/routines_notifier.dart' ;
3738import 'package:wger/providers/routines_repository.dart' ;
39+ import 'package:wger/providers/trophy_repository.dart' ;
3840import 'package:wger/providers/workout_session_repository.dart' ;
3941import 'package:wger/screens/gym_mode.dart' ;
4042import 'package:wger/screens/routine_screen.dart' ;
@@ -52,7 +54,7 @@ import '../../../test_data/routines.dart';
5254import '../../fake_connectivity.dart' ;
5355import 'gym_mode_test.mocks.dart' ;
5456
55- @GenerateMocks ([WorkoutSessionRepository , ExerciseRepository , RoutinesRepository ])
57+ @GenerateMocks ([WorkoutSessionRepository , ExerciseRepository , RoutinesRepository , TrophyRepository ])
5658void main () {
5759 installFakeConnectivity ();
5860
@@ -89,13 +91,18 @@ void main() {
8991 ).thenAnswer ((_) async => testRoutine);
9092 });
9193
92- Widget renderGymMode ({locale = 'en' , bool isOnline = true }) {
94+ Widget renderGymMode ({
95+ locale = 'en' ,
96+ bool isOnline = true ,
97+ List <riverpod.Override > extraOverrides = const [],
98+ }) {
9399 return riverpod.ProviderScope (
94100 overrides: [
95101 networkStatusProvider.overrideWithValue (isOnline),
96102 routinesRepositoryProvider.overrideWithValue (mockRoutinesRepo),
97103 exerciseRepositoryProvider.overrideWithValue (mockExerciseRepo),
98104 workoutSessionRepositoryProvider.overrideWithValue (mockSessionRepo),
105+ ...extraOverrides,
99106 // The repetition + weight unit catalogues are tiny direct-Drift
100107 // stream providers, overriding them inline is the established
101108 // pattern (see also [exerciseCategoriesProvider] etc.).
@@ -365,6 +372,79 @@ void main() {
365372 });
366373 });
367374
375+ testWidgets ('offline summary shows the local session stats' , (WidgetTester tester) async {
376+ // The trophy fetch is REST-only; offline it is skipped so the locally
377+ // stored session stats (duration, volume) render right away instead of
378+ // waiting behind a doomed network request. The clock matches a session in
379+ // the test routine so there is data to show.
380+ await withClock (Clock .fixed (DateTime (2021 , 5 , 1 , 14 , 33 )), () async {
381+ await tester.pumpWidget (renderGymMode (isOnline: false ));
382+ await tester.pumpAndSettle ();
383+
384+ // Prime the keepAlive routines stream (see the offline test above).
385+ final container = riverpod.ProviderScope .containerOf (
386+ tester.element (find.byType (TextButton )),
387+ );
388+ container.listen (routinesRiverpodProvider, (_, _) {});
389+ await tester.pumpAndSettle ();
390+
391+ await tester.tap (find.byType (TextButton ));
392+ await tester.pumpAndSettle ();
393+
394+ // Jump straight to the summary via the menu's "End workout" shortcut.
395+ await tester.tap (find.byIcon (Icons .menu));
396+ await tester.pumpAndSettle ();
397+ await tester.tap (find.text ('End workout' ));
398+ await tester.pumpAndSettle ();
399+
400+ expect (find.byType (WorkoutSummary ), findsOneWidget);
401+ expect (find.byType (StreamErrorIndicator ), findsNothing);
402+ expect (find.text ('Duration' ), findsOneWidget);
403+ expect (find.text ('Volume' ), findsOneWidget);
404+ });
405+ });
406+
407+ testWidgets ('summary surfaces an unexpected trophy-fetch error' , (WidgetTester tester) async {
408+ // Network/server errors are swallowed by the repository, so an error that
409+ // does reach the summary is a genuine exception and must be shown, not
410+ // hidden behind the stats.
411+ final mockTrophyRepo = MockTrophyRepository ();
412+ when (mockTrophyRepo.fetchTrophies (language: anyNamed ('language' ))).thenAnswer ((_) async => []);
413+ when (
414+ mockTrophyRepo.fetchProgression (
415+ filterQuery: anyNamed ('filterQuery' ),
416+ language: anyNamed ('language' ),
417+ ),
418+ ).thenAnswer ((_) async => []);
419+ when (
420+ mockTrophyRepo.fetchUserTrophies (
421+ filterQuery: anyNamed ('filterQuery' ),
422+ language: anyNamed ('language' ),
423+ ),
424+ ).thenThrow (Exception ('unexpected' ));
425+
426+ await withClock (Clock .fixed (DateTime (2021 , 5 , 1 , 14 , 33 )), () async {
427+ await tester.pumpWidget (
428+ renderGymMode (
429+ extraOverrides: [trophyRepositoryProvider.overrideWithValue (mockTrophyRepo)],
430+ ),
431+ );
432+ await tester.pumpAndSettle ();
433+ await tester.tap (find.byType (TextButton ));
434+ await tester.pumpAndSettle ();
435+
436+ // Jump straight to the summary via the menu's "End workout" shortcut.
437+ await tester.tap (find.byIcon (Icons .menu));
438+ await tester.pumpAndSettle ();
439+ await tester.tap (find.text ('End workout' ));
440+ await tester.pumpAndSettle ();
441+
442+ expect (find.byType (WorkoutSummary ), findsOneWidget);
443+ expect (find.byType (StreamErrorIndicator ), findsOneWidget);
444+ expect (find.text ('Duration' ), findsNothing);
445+ });
446+ });
447+
368448 testWidgets (
369449 'fresh session with exercise pages off: first swipe to a log page does not crash' ,
370450 (WidgetTester tester) async {
0 commit comments