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>
119 lines
3.5 KiB
Dart
119 lines
3.5 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|