mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Cover workout_screen.dart to 100%
Add live-workout interaction tests: set-tap breaks + Skip, warmup break, rep decrement recompute, long-press reset, in-workout threshold change, finish -> summary dialog, reset-confirm clears the session, and restoring an in-progress break through to _onBreakFinished. Interaction taps run under runAsync so their sqflite writes/break timers stay on the real loop. coverage:ignore the Android/iOS-only Vibration.vibrate call. 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:
parent
a27efefefc
commit
79c7d8d1c6
@ -301,7 +301,11 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
Future<void> _onBreakFinished() async {
|
||||
await _audio.play(AssetSource('sounds/break_end.mp3')).catchError((_) {});
|
||||
if (await Vibration.hasVibrator()) {
|
||||
// Android/iOS-only: hasVibrator() returns false on the Linux test host
|
||||
// (no Platform.isAndroid/isIOS), so this body never runs there.
|
||||
// coverage:ignore-start
|
||||
unawaited(Vibration.vibrate(duration: 800));
|
||||
// coverage:ignore-end
|
||||
}
|
||||
setState(() {
|
||||
_breakForExIdx = -1;
|
||||
|
||||
@ -2,14 +2,32 @@ 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/exercise.dart';
|
||||
import 'package:workout_app/models/workout_plan.dart';
|
||||
import 'package:workout_app/screens/workout_screen.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
import 'package:workout_app/widgets/break_banner.dart';
|
||||
import 'package:workout_app/widgets/exercise_tile.dart';
|
||||
import 'package:workout_app/widgets/rep_circle.dart';
|
||||
import 'package:workout_app/widgets/workout_summary_dialog.dart';
|
||||
|
||||
import '../fake_audio_platform.dart';
|
||||
import '../fake_secure_storage.dart';
|
||||
|
||||
/// A saved state with every set of the two [_exercises] tapped as done.
|
||||
Map<String, dynamic> _completeSaved() => {
|
||||
'workoutType': 'A',
|
||||
'startTimeMs': DateTime.now().millisecondsSinceEpoch,
|
||||
'tapped': [
|
||||
[true, true, true],
|
||||
[true, true, true],
|
||||
],
|
||||
'doneReps': [
|
||||
[5, 5, 5],
|
||||
[5, 5, 5],
|
||||
],
|
||||
'warmupTapped': [false, false],
|
||||
};
|
||||
|
||||
const _exercises = [
|
||||
Exercise(name: 'Squat', sets: 3, reps: 5, weight: 20.0),
|
||||
Exercise(name: 'Press', sets: 3, reps: 5, weight: 15.0),
|
||||
@ -155,4 +173,179 @@ void main() {
|
||||
await _pump(tester, _wrap(type: 'B'));
|
||||
expect(find.textContaining('Workout B'), findsOneWidget);
|
||||
});
|
||||
|
||||
// Interaction taps trigger `unawaited(_saveActiveSession())` (a sqflite write)
|
||||
// and start `Timer.periodic` breaks. Dispatching them inside runAsync keeps
|
||||
// those on the real loop, so they complete/cancel instead of lingering as
|
||||
// FakeAsync "pending timers" that fail the test.
|
||||
Future<void> _tapReal(WidgetTester tester, Finder f) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(f);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
// tester.longPress() does internal fake-clock pumps that conflict with
|
||||
// runAsync, so drive a manual gesture whose hold exceeds the long-press
|
||||
// timeout on the real clock (keeping the resulting DB write on the real loop).
|
||||
Future<void> _longPressReal(WidgetTester tester, Finder f) async {
|
||||
final center = tester.getCenter(f);
|
||||
await tester.runAsync(() async {
|
||||
final gesture = await tester.startGesture(center);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||
await gesture.up();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('tapping a set starts a break which Skip cancels', (tester) async {
|
||||
await _pump(tester, _wrap());
|
||||
await _tapReal(tester, find.byType(RepCircle).first);
|
||||
expect(find.byType(BreakBanner), findsOneWidget);
|
||||
// Success break (5 done >= 5 target).
|
||||
expect(find.textContaining('well done'), findsOneWidget);
|
||||
|
||||
await _tapReal(tester, find.text('Skip'));
|
||||
expect(find.byType(BreakBanner), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tapping warmup starts a warmup break', (tester) async {
|
||||
await _pump(tester, _wrap());
|
||||
await _tapReal(tester, find.byIcon(Icons.fitness_center).first);
|
||||
expect(find.textContaining('Warmup rest'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('re-tapping a set decrements reps and recomputes the break',
|
||||
(tester) async {
|
||||
await _pump(tester, _wrap());
|
||||
final circle = find.byType(RepCircle).first;
|
||||
await _tapReal(tester, circle); // done=5 -> success break
|
||||
expect(find.textContaining('well done'), findsOneWidget);
|
||||
|
||||
await _tapReal(tester, circle); // done 5 -> 4, below target -> fail break
|
||||
expect(find.textContaining('keep going'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('long-pressing a set resets it and cancels its break',
|
||||
(tester) async {
|
||||
await _pump(tester, _wrap());
|
||||
final circle = find.byType(RepCircle).first;
|
||||
await _tapReal(tester, circle);
|
||||
expect(find.byType(BreakBanner), findsOneWidget);
|
||||
|
||||
await _longPressReal(tester, circle);
|
||||
expect(find.byType(BreakBanner), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('finishing a completed workout saves and shows the summary',
|
||||
(tester) async {
|
||||
await _pump(tester, _wrap(savedState: _completeSaved()));
|
||||
|
||||
// Drive the whole flow on the real loop: _confirmFinish's showDialog await
|
||||
// resumes into _finishWorkout in the same zone, and _finishWorkout does
|
||||
// several sqflite writes that would hang under FakeAsync.
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Finish'));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
await tester.pump();
|
||||
expect(find.text('Finish workout?'), findsOneWidget);
|
||||
// The dialog's Finish button is the last 'Finish' in the tree.
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Finish').last);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 800));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.byType(WorkoutSummaryDialog), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('confirming Reset clears the active session', (tester) async {
|
||||
await tester.runAsync(
|
||||
() => StorageService.instance.saveActiveSession({
|
||||
'workoutType': 'A',
|
||||
'startTimeMs': DateTime.now().millisecondsSinceEpoch,
|
||||
'tapped': [
|
||||
[false, false, false],
|
||||
[false, false, false],
|
||||
],
|
||||
'doneReps': [
|
||||
[5, 5, 5],
|
||||
[5, 5, 5],
|
||||
],
|
||||
'warmupTapped': [false, false],
|
||||
}),
|
||||
);
|
||||
await _pump(tester, _wrap());
|
||||
|
||||
// Opening the confirm dialog does no DB work.
|
||||
await tester.tap(find.text('Reset'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(find.text('Reset workout?'), findsOneWidget);
|
||||
|
||||
// Confirm: clearActiveSession is a DB write, so run on the real loop.
|
||||
// (The dialog's Reset button is the last 'Reset' in the tree.)
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Reset').last);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
});
|
||||
await tester.pump();
|
||||
|
||||
final session = await tester.runAsync(
|
||||
() => StorageService.instance.loadActiveSession(),
|
||||
);
|
||||
expect(session, isNull); // reset cleared the persisted session
|
||||
});
|
||||
|
||||
testWidgets('changing a threshold in the workout persists it', (tester) async {
|
||||
// A real exercise has a progression-state row, so _onThresholdChanged's
|
||||
// state-update branch runs (and the write actually lands).
|
||||
final name = workoutA.first.name;
|
||||
await _pump(tester, _wrap(exercises: [workoutA.first]));
|
||||
|
||||
// The success stepper's "+" is the first Icons.add in the single tile.
|
||||
await _tapReal(tester, find.byIcon(Icons.add).first);
|
||||
|
||||
final state = await tester.runAsync(
|
||||
() => StorageService.instance.getExerciseState(name),
|
||||
);
|
||||
expect(state!.successThreshold, 4); // default 3 -> +1
|
||||
});
|
||||
|
||||
testWidgets('restores an in-progress break and finishes it', (tester) async {
|
||||
final now = DateTime.now();
|
||||
final saved = {
|
||||
'workoutType': 'A',
|
||||
'startTimeMs': now.millisecondsSinceEpoch,
|
||||
'tapped': [
|
||||
[true, false, false],
|
||||
[false, false, false],
|
||||
],
|
||||
'doneReps': [
|
||||
[5, 5, 5],
|
||||
[5, 5, 5],
|
||||
],
|
||||
'warmupTapped': [false, false],
|
||||
'breakForExIdx': 0,
|
||||
'breakForSetIdx': 0,
|
||||
'breakLabel': 'Rest',
|
||||
'breakDurationSecs': 180,
|
||||
'breakEndMs': now.add(const Duration(seconds: 2)).millisecondsSinceEpoch,
|
||||
};
|
||||
await _pump(tester, _wrap(savedState: saved));
|
||||
expect(find.byType(BreakBanner), findsOneWidget); // break restored
|
||||
|
||||
// The break timer is a real periodic timer (initState ran under _pump's
|
||||
// runAsync). Wait out its ~2s remaining so _tickBreak reaches 0 and
|
||||
// _onBreakFinished (sound + vibration) runs on the real loop.
|
||||
await tester.runAsync(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 3));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(find.byType(BreakBanner), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user