mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 13:23:01 +02:00
367 lines
9.6 KiB
Dart
367 lines
9.6 KiB
Dart
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|