Cover history_screen.dart to 100%

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
This commit is contained in:
Krzysztof Rudnicki 2026-07-13 14:10:21 +02:00
parent e14cdf9997
commit a27efefefc
2 changed files with 128 additions and 0 deletions

View File

@ -51,7 +51,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
}
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(() {

View File

@ -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,
},
]
: <Map<String, dynamic>>[],
},
],
});
}
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<String>));
await Future<void>.delayed(const Duration(milliseconds: 400));
await tester.pump();
await tester.tap(find.text(name).last);
await Future<void>.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': <Map<String, dynamic>>[],
},
],
});
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
});
}