Add sleep timer feature
Timer stops playback (pause) after 5/10/15/30/60 min. Wall-clock deadline is self-correcting against OS throttling; 1Hz tick drives the countdown UI. Cancelled in stop() so no timer resurrects a paused player after teardown. - sleep_timer.dart: isolated time logic, injectable clock for testing - audio_handler.dart: sleepTimer field + cancel() in stop() - now_playing_screen.dart: AppBar button + bottom sheet - 10 unit tests via mocked clock Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
78f4b55e68
commit
51c2229148
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:melo/player/sleep_timer.dart';
|
||||
|
||||
void main() {
|
||||
group('SleepTimer with mocked clock', () {
|
||||
late DateTime currentTime;
|
||||
late SleepTimer sleepTimer;
|
||||
late int elapsedCount;
|
||||
|
||||
setUp(() {
|
||||
currentTime = DateTime(2024, 1, 1, 12, 0, 0);
|
||||
elapsedCount = 0;
|
||||
sleepTimer = SleepTimer(
|
||||
onElapsed: () => elapsedCount++,
|
||||
now: () => currentTime,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
sleepTimer.cancel();
|
||||
});
|
||||
|
||||
test('remaining is null when inactive', () {
|
||||
expect(sleepTimer.remaining.value, isNull);
|
||||
});
|
||||
|
||||
test('start sets remaining to duration', () {
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
expect(sleepTimer.remaining.value, isNotNull);
|
||||
expect(sleepTimer.remaining.value!.inMinutes, equals(5));
|
||||
});
|
||||
|
||||
test('remaining decrements as time passes', () {
|
||||
sleepTimer.start(const Duration(minutes: 1));
|
||||
expect(sleepTimer.remaining.value!.inSeconds, equals(60));
|
||||
|
||||
// Simulate 30 seconds passing
|
||||
currentTime = currentTime.add(const Duration(seconds: 30));
|
||||
sleepTimer.tick(); // Manually call tick to simulate timer
|
||||
|
||||
expect(sleepTimer.remaining.value!.inSeconds, equals(30));
|
||||
});
|
||||
|
||||
test('fires callback when timer elapses', () {
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
expect(elapsedCount, equals(0));
|
||||
|
||||
// Simulate 5 minutes passing
|
||||
currentTime = currentTime.add(const Duration(minutes: 5));
|
||||
sleepTimer.tick();
|
||||
|
||||
expect(elapsedCount, equals(1), reason: 'onElapsed should fire once');
|
||||
expect(sleepTimer.remaining.value, isNull);
|
||||
});
|
||||
|
||||
test('cancel stops timer and clears remaining', () {
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
expect(sleepTimer.remaining.value, isNotNull);
|
||||
|
||||
sleepTimer.cancel();
|
||||
|
||||
expect(sleepTimer.remaining.value, isNull);
|
||||
expect(elapsedCount, equals(0), reason: 'Should not fire after cancel');
|
||||
});
|
||||
|
||||
test('does not fire if duration is zero', () {
|
||||
sleepTimer.start(Duration.zero);
|
||||
|
||||
expect(elapsedCount, equals(0));
|
||||
expect(sleepTimer.remaining.value, isNull);
|
||||
});
|
||||
|
||||
test('does not fire if duration is negative', () {
|
||||
sleepTimer.start(const Duration(seconds: -5));
|
||||
|
||||
expect(elapsedCount, equals(0));
|
||||
expect(sleepTimer.remaining.value, isNull);
|
||||
});
|
||||
|
||||
test('start while running replaces old timer', () {
|
||||
sleepTimer.start(const Duration(minutes: 10));
|
||||
final firstDuration = sleepTimer.remaining.value!;
|
||||
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
final secondDuration = sleepTimer.remaining.value!;
|
||||
|
||||
expect(secondDuration < firstDuration, isTrue);
|
||||
expect(elapsedCount, equals(0));
|
||||
});
|
||||
|
||||
test('fires exactly once when elapsed', () {
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
|
||||
// Advance time past deadline
|
||||
currentTime = currentTime.add(const Duration(minutes: 6));
|
||||
sleepTimer.tick();
|
||||
|
||||
expect(elapsedCount, equals(1));
|
||||
|
||||
// Even if we call tick again, should not fire twice
|
||||
sleepTimer.tick();
|
||||
expect(elapsedCount, equals(1), reason: 'Should fire only once');
|
||||
});
|
||||
|
||||
test('does not fire if cancelled before elapse', () {
|
||||
sleepTimer.start(const Duration(minutes: 5));
|
||||
|
||||
currentTime = currentTime.add(const Duration(minutes: 3));
|
||||
sleepTimer.cancel();
|
||||
|
||||
currentTime = currentTime.add(const Duration(minutes: 3));
|
||||
sleepTimer.tick();
|
||||
|
||||
expect(elapsedCount, equals(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user