From a27efefefcd723c608154102665732bc296a9b59 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 13 Jul 2026 14:10:21 +0200 Subject: [PATCH] Cover history_screen.dart to 100% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an exercise drill-down test (dropdown select -> streak stats card, weight chart, exercise calendar nav, session tiles with warmup/reps variants) and a same-date test for the chart painter's tRange==0 branch. coverage:ignore the _load exercise-state branch (unreachable — selection goes through _pickExercise, not _load). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH --- .../lib/screens/history_screen.dart | 5 + .../test/screens/history_screen_test.dart | 123 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/stronglift_replacement/workout_app/lib/screens/history_screen.dart b/stronglift_replacement/workout_app/lib/screens/history_screen.dart index 6001984..05a2856 100644 --- a/stronglift_replacement/workout_app/lib/screens/history_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/history_screen.dart @@ -51,7 +51,12 @@ class _HistoryScreenState extends State { } ExerciseState? state; if (_selected != _kTotal) { + // Unreachable in practice: _load only runs from initState, where + // _selected is still _kTotal; exercise selection goes through + // _pickExercise, which loads its own state. + // coverage:ignore-start state = await StorageService.instance.getExerciseState(_selected); + // coverage:ignore-end } if (mounted) { setState(() { diff --git a/stronglift_replacement/workout_app/test/screens/history_screen_test.dart b/stronglift_replacement/workout_app/test/screens/history_screen_test.dart index 5a7434e..22a2428 100644 --- a/stronglift_replacement/workout_app/test/screens/history_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/history_screen_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; 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/history_screen.dart'; import 'package:workout_app/services/storage_service.dart'; @@ -153,4 +154,126 @@ void main() { await _pump(tester, _wrap()); expect(find.byType(HistoryScreen), findsOneWidget); }); + + // JSON for one session containing [name] at [weight]. + String _exerciseSessionJson( + String name, + double weight, { + required bool ok, + required bool warmup, + required bool withSets, + }) { + return jsonEncode({ + 'exercises': [ + { + 'name': name, + 'targetSets': 3, + 'targetReps': 5, + 'targetWeight': weight, + 'warmupDone': warmup, + 'succeeded': ok, + 'sets': withSets + ? [ + { + 'targetReps': 5, + 'doneReps': 5, + 'weight': weight, + 'succeeded': true, + }, + { + 'targetReps': 5, + 'doneReps': 4, + 'weight': weight, + 'succeeded': false, + }, + ] + : >[], + }, + ], + }); + } + + testWidgets('selecting an exercise shows the drill-down view', (tester) async { + // Use a real progression-tracked exercise so getExerciseState is non-null + // and the streak stats card renders. Three distinct weights on three dates + // give the weight chart >= 2 points with a real range (painter fully runs). + final name = workoutA.first.name; + await _seed( + tester, + _exerciseSessionJson(name, 20, ok: true, warmup: true, withSets: true), + date: '2024-06-01', + ); + await _seed( + tester, + _exerciseSessionJson(name, 22.5, ok: false, warmup: false, withSets: false), + date: '2024-06-08', + succeeded: false, + ); + await _seed( + tester, + _exerciseSessionJson(name, 25, ok: true, warmup: true, withSets: true), + date: '2024-06-15', + ); + + await _pump(tester, _wrap()); + + // Open the dropdown and pick the exercise; _pickExercise awaits a DB read, + // and the menu open/close animation runs on real timers under runAsync, so + // drive the whole interaction on the real loop. + await tester.runAsync(() async { + await tester.tap(find.byType(DropdownButton)); + await Future.delayed(const Duration(milliseconds: 400)); + await tester.pump(); + await tester.tap(find.text(name).last); + await Future.delayed(const Duration(milliseconds: 400)); + await tester.pump(); + }); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + // Exercise view rendered: chart + streak stats card near the top. + expect(find.text('WEIGHT OVER TIME'), findsOneWidget); + expect(find.textContaining('more to'), findsWidgets); + + // Exercise-view calendar month nav (covers its onPrev/onNext closures). + await tester.tap(find.byIcon(Icons.chevron_right).first); + await tester.pump(); + await tester.tap(find.byIcon(Icons.chevron_left).first); + await tester.pump(); + + // The session list + name label sit below the fold; scroll to build them. + await tester.scrollUntilVisible( + find.text(name.toUpperCase()), + 300, + scrollable: find.byType(Scrollable).first, + ); + expect(find.text(name.toUpperCase()), findsOneWidget); + // Session tiles: warmup marker and reps summary both present. + expect(find.textContaining('warmup'), findsWidgets); + expect(find.textContaining('reps:'), findsWidgets); + }); + + testWidgets('chart handles two points on the same date (tRange == 0)', + (tester) async { + // Two sessions on the SAME date give the total chart two points that share + // a timestamp, exercising the painter's tRange == 0 (centre-x) branch. + String vol(double w) => jsonEncode({ + 'exercises': [ + { + 'name': 'Squat', + 'targetSets': 3, + 'targetReps': 5, + 'targetWeight': w, + 'warmupDone': false, + 'succeeded': true, + 'sets': >[], + }, + ], + }); + await _seed(tester, vol(20), date: '2024-06-01'); + await _seed(tester, vol(40), date: '2024-06-01'); + await _pump(tester, _wrap()); + expect(find.byType(HistoryScreen), findsOneWidget); + expect(find.text('WEIGHT OVER TIME').evaluate(), isEmpty); // total view + }); }