testsAndMisc-archive/pomodoro_app/test/services/pomodoro_timer_test.dart

367 lines
9.6 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:pomodoro_app/models/pomodoro_state.dart';
import 'package:pomodoro_app/services/pomodoro_timer.dart';
/// A controllable fake timer for testing.
class FakeTimerController {
void Function(Timer)? _callback;
bool _isActive = true;
void tick() {
if (_isActive) {
_callback?.call(_FakeTimer(this));
}
}
void cancel() {
_isActive = false;
}
bool get isActive => _isActive;
}
class _FakeTimer implements Timer {
_FakeTimer(this._controller);
final FakeTimerController _controller;
@override
void cancel() => _controller.cancel();
@override
bool get isActive => _controller.isActive;
@override
int get tick => 0;
}
void main() {
late PomodoroTimer timer;
late FakeTimerController fakeController;
Timer fakeTimerFactory(Duration duration, void Function(Timer) callback) {
fakeController = FakeTimerController();
fakeController._callback = callback;
return _FakeTimer(fakeController);
}
setUp(() {
timer = PomodoroTimer(
workMinutes: 1, // 60 seconds for faster testing
shortBreakMinutes: 1,
longBreakMinutes: 2,
pomodorosPerCycle: 2,
timerFactory: fakeTimerFactory,
);
});
tearDown(() {
timer.dispose();
});
group('Initial state', () {
test('starts in work mode', () {
expect(timer.state.mode, PomodoroMode.work);
});
test('is not running', () {
expect(timer.state.isRunning, false);
});
test('has correct initial time', () {
expect(timer.state.remainingSeconds, 60);
expect(timer.state.totalSeconds, 60);
});
test('has zero completed pomodoros', () {
expect(timer.state.completedPomodoros, 0);
});
});
group('start()', () {
test('sets isRunning to true', () {
timer.start();
expect(timer.state.isRunning, true);
});
test('does nothing if already running', () {
timer.start();
final stateAfterFirstStart = timer.state;
timer.start(); // second call
expect(timer.state, stateAfterFirstStart);
});
test('notifies listeners', () {
var notified = false;
timer.addListener(() => notified = true);
timer.start();
expect(notified, true);
});
});
group('pause()', () {
test('sets isRunning to false', () {
timer.start();
timer.pause();
expect(timer.state.isRunning, false);
});
test('does nothing if already paused', () {
final state = timer.state;
timer.pause();
expect(timer.state, state);
});
test('preserves remaining time', () {
timer.start();
fakeController.tick(); // -1s
fakeController.tick(); // -1s
timer.pause();
expect(timer.state.remainingSeconds, 58);
});
});
group('Ticking', () {
test('decrements remaining seconds', () {
timer.start();
fakeController.tick();
expect(timer.state.remainingSeconds, 59);
});
test('notifies on each tick', () {
timer.start();
var count = 0;
timer.addListener(() => count++);
fakeController.tick();
fakeController.tick();
expect(count, 2);
});
});
group('Session completion', () {
test('transitions from work to short break', () {
timer.start();
// Tick down to 1 second, then one more tick completes.
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.mode, PomodoroMode.shortBreak);
expect(timer.state.isRunning, false);
expect(timer.state.completedPomodoros, 1);
});
test('transitions to long break after cycle', () {
// Complete 2 pomodoros (pomodorosPerCycle = 2).
// First pomodoro.
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.mode, PomodoroMode.shortBreak);
// Skip break.
timer.skip();
expect(timer.state.mode, PomodoroMode.work);
// Second pomodoro.
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.mode, PomodoroMode.longBreak);
expect(timer.state.completedPomodoros, 2);
});
test('transitions from break to work', () {
// Complete a work session.
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.mode, PomodoroMode.shortBreak);
// Complete the break.
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.mode, PomodoroMode.work);
});
});
group('reset()', () {
test('resets to full duration', () {
timer.start();
fakeController.tick();
fakeController.tick();
timer.reset();
expect(timer.state.remainingSeconds, 60);
expect(timer.state.isRunning, false);
});
test('keeps the current mode', () {
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
// Now in short break mode.
timer.reset();
expect(timer.state.mode, PomodoroMode.shortBreak);
});
});
group('skip()', () {
test('skips from work to short break', () {
timer.skip();
expect(timer.state.mode, PomodoroMode.shortBreak);
expect(timer.state.isRunning, false);
});
test('skips from break to work', () {
timer.skip(); // work -> short break
timer.skip(); // short break -> work
expect(timer.state.mode, PomodoroMode.work);
});
test('stops the timer when skipping', () {
timer.start();
timer.skip();
expect(timer.state.isRunning, false);
});
});
group('dispose()', () {
test('cancels internal timer', () {
// Create a separate timer so tearDown does not double-dispose.
final disposableTimer = PomodoroTimer(
workMinutes: 1,
shortBreakMinutes: 1,
longBreakMinutes: 2,
pomodorosPerCycle: 2,
timerFactory: fakeTimerFactory,
);
disposableTimer.start();
disposableTimer.dispose();
expect(fakeController.isActive, false);
});
});
2026-02-21 20:40:33 +01:00
group('switchStyle()', () {
test('switches to ultraradian with correct durations', () {
timer.switchStyle(TimerStyle.ultraradian);
expect(timer.timerStyle, TimerStyle.ultraradian);
expect(timer.state.remainingSeconds, 90 * 60);
expect(timer.state.totalSeconds, 90 * 60);
expect(timer.state.pomodorosPerCycle, 1);
expect(timer.state.mode, PomodoroMode.work);
expect(timer.state.isRunning, false);
});
test('switches back to pomodoro', () {
timer.switchStyle(TimerStyle.ultraradian);
timer.switchStyle(TimerStyle.pomodoro);
expect(timer.timerStyle, TimerStyle.pomodoro);
expect(timer.state.remainingSeconds, 25 * 60);
expect(timer.state.totalSeconds, 25 * 60);
expect(timer.state.pomodorosPerCycle, 4);
});
test('resets running timer when switching', () {
timer.start();
fakeController.tick();
expect(timer.state.isRunning, true);
timer.switchStyle(TimerStyle.ultraradian);
expect(timer.state.isRunning, false);
expect(timer.state.remainingSeconds, 90 * 60);
});
test('does nothing when switching to same style', () {
timer.start();
fakeController.tick();
final stateBefore = timer.state;
timer.switchStyle(TimerStyle.pomodoro);
expect(timer.state, stateBefore);
});
test('notifies listeners', () {
var notified = false;
timer.addListener(() => notified = true);
timer.switchStyle(TimerStyle.ultraradian);
expect(notified, true);
});
test('resets completed pomodoros', () {
timer.start();
for (var i = 0; i < 60; i++) {
fakeController.tick();
}
expect(timer.state.completedPomodoros, 1);
timer.switchStyle(TimerStyle.ultraradian);
expect(timer.state.completedPomodoros, 0);
});
});
group('timerStyle getter', () {
test('defaults to pomodoro', () {
expect(timer.timerStyle, TimerStyle.pomodoro);
});
});
group('applyRemoteState()', () {
test('applies remote state and notifies listeners', () {
var notified = false;
timer.addListener(() => notified = true);
final remoteState = PomodoroState(
mode: PomodoroMode.shortBreak,
remainingSeconds: 200,
totalSeconds: 300,
isRunning: false,
completedPomodoros: 2,
pomodorosPerCycle: 4,
);
timer.applyRemoteState(remoteState, 'pause');
expect(timer.state.mode, PomodoroMode.shortBreak);
expect(timer.state.remainingSeconds, 200);
expect(timer.state.completedPomodoros, 2);
expect(timer.state.isRunning, false);
expect(notified, true);
});
test('starts local ticking when remote state is running', () {
final remoteState = PomodoroState(
mode: PomodoroMode.work,
remainingSeconds: 500,
totalSeconds: 600,
isRunning: true,
completedPomodoros: 0,
pomodorosPerCycle: 4,
);
timer.applyRemoteState(remoteState, 'start');
expect(timer.state.isRunning, true);
// The fake timer should have been created; ticking should work.
fakeController.tick();
expect(timer.state.remainingSeconds, 499);
});
test('stops local ticking when remote state is paused', () {
// First start the timer locally.
timer.start();
fakeController.tick();
expect(timer.state.remainingSeconds, 59);
// Apply remote pause.
final remoteState = timer.state.copyWith(isRunning: false);
timer.applyRemoteState(remoteState, 'pause');
expect(timer.state.isRunning, false);
});
});
}