Cover home_screen.dart to 100%

Cover the post-navigation reload paths (return from workout via Resume,
return from settings), the resume-button closure, and the empty-address
placeholder. Expose ServerAddressTile so its no-address fallback can be
unit-tested directly (the test host always has network interfaces).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
This commit is contained in:
Krzysztof Rudnicki 2026-07-13 13:57:10 +02:00
parent b76d156ae3
commit 5fe4222dfd
2 changed files with 83 additions and 3 deletions

View File

@ -153,7 +153,7 @@ class _HomeScreenState extends State<HomeScreen> {
onResume: () => _openWorkout(resume: true),
),
const SizedBox(height: 20),
_ServerAddressTile(addresses: _serverAddresses),
ServerAddressTile(addresses: _serverAddresses),
],
),
),
@ -278,8 +278,12 @@ class _WorkoutCard extends StatelessWidget {
}
}
class _ServerAddressTile extends StatelessWidget {
const _ServerAddressTile({required this.addresses});
/// Displays the LAN addresses the workout HTTP server is reachable at, or a
/// "Server not started" placeholder when none are available. Public so the
/// empty-address fallback can be exercised in a widget test.
class ServerAddressTile extends StatelessWidget {
/// Creates a tile listing [addresses] (may be empty).
const ServerAddressTile({required this.addresses, super.key});
final List<String> addresses;

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:workout_app/models/workout_plan.dart';
import 'package:workout_app/screens/home_screen.dart';
import 'package:workout_app/screens/workout_screen.dart';
import 'package:workout_app/services/storage_service.dart';
import '../fake_secure_storage.dart';
@ -118,4 +120,78 @@ void main() {
// The post-frame auto-resume pushed the workout screen.
expect(find.textContaining('Workout A'), findsWidgets);
});
testWidgets('active session shows Resume, returns (98) and re-enters (153)',
(tester) async {
// A fully-formed active session so WorkoutScreen restores cleanly (its
// _restoreFromSaved expects startTimeMs + per-exercise tapped/doneReps).
await tester.runAsync(
() => StorageService.instance.saveActiveSession({
'workoutType': 'A',
'startTimeMs': DateTime.now().millisecondsSinceEpoch,
'tapped': [for (final e in workoutA) List<bool>.filled(e.sets, false)],
'doneReps': [for (final e in workoutA) List<int>.filled(e.sets, e.reps)],
'warmupTapped': List<bool>.filled(workoutA.length, false),
}),
);
// Drive the whole first-load + post-frame auto-resume push on the real loop
// (auto-resume awaits getCurrentExercises, which hangs under FakeAsync).
await tester.runAsync(() async {
await tester.pumpWidget(_wrap());
await Future<void>.delayed(const Duration(milliseconds: 300));
await tester.pump(); // fire the post-frame auto-resume callback
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(WorkoutScreen), findsOneWidget);
// Return to home; auto-resume is now consumed so the Resume button shows.
await tester.runAsync(() async {
tester.state<NavigatorState>(find.byType(Navigator).last).pop();
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(WorkoutScreen), findsNothing);
expect(find.text('Resume Workout'), findsOneWidget);
// Tapping Resume invokes onResume -> _openWorkout(resume:true) (line 153).
await tester.runAsync(() async {
await tester.tap(find.text('Resume Workout'));
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(WorkoutScreen), findsOneWidget);
});
testWidgets('returning from settings reloads the home screen',
(tester) async {
installFakeSecureStorage(); // SettingsScreen reads the sync token on init
await _pump(tester, _wrap());
await tester.tap(find.byIcon(Icons.settings));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Settings'), findsOneWidget);
// Pop back; the trailing _load() (line 135) needs the real loop for DB I/O.
await tester.runAsync(() async {
tester.state<NavigatorState>(find.byType(Navigator).last).pop();
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Workout Tracker'), findsOneWidget);
});
testWidgets('ServerAddressTile shows a placeholder when no addresses',
(tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: ServerAddressTile(addresses: <String>[])),
),
);
expect(find.text('Server not started'), findsOneWidget);
});
}