From 5fe4222dfdef9f66067d89a834bf3ff0495e783e Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 13 Jul 2026 13:57:10 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH --- .../workout_app/lib/screens/home_screen.dart | 10 ++- .../test/screens/home_screen_test.dart | 76 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen.dart b/stronglift_replacement/workout_app/lib/screens/home_screen.dart index 4567caa..4f11308 100644 --- a/stronglift_replacement/workout_app/lib/screens/home_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/home_screen.dart @@ -153,7 +153,7 @@ class _HomeScreenState extends State { 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 addresses; diff --git a/stronglift_replacement/workout_app/test/screens/home_screen_test.dart b/stronglift_replacement/workout_app/test/screens/home_screen_test.dart index 047b3cb..6016324 100644 --- a/stronglift_replacement/workout_app/test/screens/home_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/home_screen_test.dart @@ -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.filled(e.sets, false)], + 'doneReps': [for (final e in workoutA) List.filled(e.sets, e.reps)], + 'warmupTapped': List.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.delayed(const Duration(milliseconds: 300)); + await tester.pump(); // fire the post-frame auto-resume callback + await Future.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(find.byType(Navigator).last).pop(); + await Future.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.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(find.byType(Navigator).last).pop(); + await Future.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: [])), + ), + ); + expect(find.text('Server not started'), findsOneWidget); + }); }