From d27eafdca6886fbc07e8feb6a1b7987a5c2f318d Mon Sep 17 00:00:00 2001 From: "Mateus Felipe C. C. Pinto" Date: Sun, 29 Oct 2023 09:04:33 -0300 Subject: [PATCH] Implement periodic timer (#64) --- CHANGELOG.md | 5 + example/countdown.dart | 18 +- lib/pausable_timer.dart | 84 ++++- pubspec.yaml | 2 +- test/pausable_timer_test.dart | 621 ++++++++++++++++++++++++++-------- 5 files changed, 556 insertions(+), 174 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4996a1..caab207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.1.0 + +- Provide `.periodic` constructor (#45) + - A periodic timer should behave similar to [Timer.periodic](https://api.dart.dev/stable/dart-async/Timer/Timer.periodic.html) + ## 3.0.0 - **BREAKING:** `PausableTimer` is now `final` diff --git a/example/countdown.dart b/example/countdown.dart index fe45284..20e2f95 100644 --- a/example/countdown.dart +++ b/example/countdown.dart @@ -1,4 +1,6 @@ // Example on how to implement countdown making a PausableTimer periodic. +import 'dart:async'; + import 'package:pausable_timer/pausable_timer.dart'; void main() async { @@ -7,21 +9,15 @@ void main() async { var countDown = 5; print('Create a periodic timer that fires every 1 second and starts it'); - timer = PausableTimer( + timer = PausableTimer.periodic( Duration(seconds: 1), () { countDown--; - // If we reached 0, we don't reset and restart the time, so it won't fire - // again, but it can be reused afterwards if needed. If we cancel the - // timer, then it can be reused after the countdown is over. - if (countDown > 0) { - // we know the callback won't be called before the constructor ends, so - // it is safe to use ! - timer - ..reset() - ..start(); + + if (countDown == 0) { + timer.pause(); } - // This is really what your callback do. + print('\t$countDown'); }, )..start(); diff --git a/lib/pausable_timer.dart b/lib/pausable_timer.dart index f0d5e52..aa36857 100644 --- a/lib/pausable_timer.dart +++ b/lib/pausable_timer.dart @@ -39,6 +39,8 @@ final class PausableTimer implements Timer { /// When the timer expires, this stopwatch is set to null. Stopwatch? _stopwatch = clock.stopwatch(); + final bool _periodic; + /// The currently active [Timer]. /// /// This is null whenever this timer is not currently active. @@ -61,45 +63,91 @@ final class PausableTimer implements Timer { /// should make sure the timer wasn't cancelled before calling this function. void _startTimer() { assert(_stopwatch != null); - _timer = _zone.createTimer( - _originalDuration - _stopwatch!.elapsed, - () { - _tick++; - _timer = null; - _stopwatch = null; - _zone.run(_callback!); - }, - ); + + if (_periodic && _stopwatch!.elapsed == Duration.zero) { + _timer = _zone.createPeriodicTimer( + duration, + (Timer timer) { + _tick++; + _zone.run(_callback!); + _stopwatch = clock.stopwatch(); + _stopwatch!.start(); + }, + ); + } else { + _timer = _zone.createTimer( + duration - _stopwatch!.elapsed, + () { + _tick++; + if (_periodic) { + reset(); + } else { + _timer = null; + _stopwatch = null; + } + _zone.run(_callback!); + }, + ); + } + _stopwatch!.start(); } - /// Creates a new timer. + /// Creates a new pausable timer. /// /// The [callback] is invoked after the given [duration], but can be [pause]d /// in between or [reset]. The [elapsed] time is only accounted for while the - /// timer [isActive]. + /// timer is active. + /// + /// The timer is paused when created, and must be [start]ed manually. + /// + /// The [duration] must be equals or bigger than [Duration.zero]. + /// If it is [Duration.zero], the [callback] will still not be called until + /// the timer is [start]ed. + PausableTimer(this.duration, void Function() callback) + : assert(duration >= Duration.zero), + _zone = Zone.current, + _periodic = false { + _callback = _zone.bindCallback(callback); + } + + /// Creates a new repeating pausable timer. + /// + /// The [callback] is invoked repeatedly with [duration] intervals until + /// canceled with the [cancel] function, but can be [pause]d + /// in between or [reset]. The [elapsed] time is only accounted for while the + /// timer is active. + /// + /// The timer is paused when created, and must be [start]ed manually. + /// + /// The exact timing depends on the underlying timer implementation. + /// No more than `n` callbacks will be made in `duration * n` time, + /// but the time between two consecutive callbacks + /// can be shorter and longer than `duration`. /// - /// The timer [isPaused] when created, and must be [start]ed manually. + /// In particular, an implementation may schedule the next callback, e.g., + /// a `duration` after either when the previous callback ended, + /// when the previous callback started, or when the previous callback was + /// scheduled for - even if the actual callback was delayed. /// /// The [duration] must be equals or bigger than [Duration.zero]. /// If it is [Duration.zero], the [callback] will still not be called until /// the timer is [start]ed. - PausableTimer(Duration duration, void Function() callback) + PausableTimer.periodic(this.duration, void Function() callback) : assert(duration >= Duration.zero), - _originalDuration = duration, - _zone = Zone.current { + _zone = Zone.current, + _periodic = true { _callback = _zone.bindCallback(callback); } /// The original duration this [Timer] was created with. - Duration get duration => _originalDuration; - final Duration _originalDuration; + final Duration duration; /// The time this [Timer] have been active. /// /// If the timer is paused, the elapsed time is also not computed anymore, so /// [elapsed] is always less than or equals to the [duration]. - Duration get elapsed => _stopwatch?.elapsed ?? _originalDuration; + Duration get elapsed => _stopwatch?.elapsed ?? duration; /// True if this [Timer] is armed but not currently active. /// diff --git a/pubspec.yaml b/pubspec.yaml index 65af27d..a85ea55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ funding: - https://github.com/sponsors/mateusfccp - https://github.com/llucax/llucax/blob/main/sponsoring-platforms.md -version: 3.0.0 +version: 3.1.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/test/pausable_timer_test.dart b/test/pausable_timer_test.dart index d47331c..38a0d0e 100644 --- a/test/pausable_timer_test.dart +++ b/test/pausable_timer_test.dart @@ -5,208 +5,541 @@ import 'package:pausable_timer/pausable_timer.dart'; void main() { const oneSecond = Duration(seconds: 1); + const halfSecond = Duration(milliseconds: 500); var numCalls = 0; void callback() => numCalls++; setUp(() => numCalls = 0); - void expectState( - PausableTimer timer, - Duration duration, - State state, { - Duration elapsed = Duration.zero, - int withCalls = 0, - }) { - expect(timer.isActive, state == State.active, reason: 'Property: isActive'); - expect(timer.isPaused, state == State.paused, reason: 'Property: isPaused'); - expect(timer.isExpired, state == State.expired || state == State.expiredCancelled, reason: 'Property: isExpired'); - expect(timer.isCancelled, state == State.cancelled || state == State.expiredCancelled, reason: 'Property: isCancelled'); - expect(timer.duration, duration, reason: 'Property: duration'); - expect(timer.elapsed, elapsed, reason: 'Property: elapsed'); - expect(numCalls, withCalls, reason: 'Property: numCalls'); - expect(timer.tick, numCalls, reason: 'Property: ticks'); - } - - test("A pausable timer duration can't be less than zero", () { - expect( - () => PausableTimer(Duration(seconds: -1), () {}), - throwsA(isA()), - ); - }); + group('A timer', () { + test("duration can't be less than zero", () { + expect( + () => PausableTimer(Duration(seconds: -1), callback), + throwsA(isA()), + ); + }); - test("A pausable timer should initially be paused with the duration set to the given duration", () { - for (final duration in [Duration.zero, oneSecond]) { - final timer = PausableTimer(duration, callback); - expectState(timer, duration, State.paused); - } - }); + test("duration should be set to the given duration", () { + for (final duration in [Duration.zero, oneSecond]) { + final timer = PausableTimer(duration, callback); + expect(timer.duration, duration); + } + }); - test('start()', () { - fakeAsync((fakeTime) { - final timer = PausableTimer(oneSecond, callback); + test("shouldn't start automatically", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + expect(timer.isActive, isFalse); - // Wait for a couple of seconds and it should be still paused - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.paused); + async.elapse(oneSecond * 2); + expect(timer.isActive, isFalse); + }); + }); - timer.start(); - expectState(timer, oneSecond, State.active); + test("should call it's callback after the given duration", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); - // Wait for half the duration, it should be still running - fakeTime.elapse(oneSecond ~/ 2); - expectState(timer, oneSecond, State.active, elapsed: oneSecond ~/ 2); + timer.start(); + async.elapse(oneSecond - Duration(microseconds: 1)); + expect(numCalls, 0); + expect(timer.isExpired, isFalse); - // start again should do nothing - timer.start(); - expectState(timer, oneSecond, State.active, elapsed: oneSecond ~/ 2); + async.elapse(Duration(microseconds: 1)); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); + }); + }); - // Wait again for half the duration and it should have expired - fakeTime.elapse(oneSecond ~/ 2); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + test("should do nothing after it's expired", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); - // Wait for a couple more seconds, nothing should happen - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + timer.start(); + async.elapse(oneSecond); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); - // start when it's already expired should do nothing - timer.start(); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + async.elapse(oneSecond * 2); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); + }); }); }); - test('pause()', () { - fakeAsync((fakeTime) { - final timer = PausableTimer(oneSecond, callback); + group('A periodic timer', () { + test("duration can't be less than zero", () { + expect( + () => PausableTimer.periodic(Duration(seconds: -1), callback), + throwsA(isA()), + ); + }); - timer.start(); - expectState(timer, oneSecond, State.active); + test("duration should be set to the given duration", () { + for (final duration in [Duration.zero, oneSecond]) { + final timer = PausableTimer.periodic(duration, callback); + expect(timer.duration, duration); + } + }); - // Wait for half the duration, it should be still running - fakeTime.elapse(oneSecond ~/ 2); - expectState(timer, oneSecond, State.active, elapsed: oneSecond ~/ 2); - var elapsed = timer.elapsed; + test("shouldn't start automatically", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + expect(timer.isActive, isFalse); - // Pause it - timer.pause(); - expectState(timer, oneSecond, State.paused, elapsed: elapsed); - elapsed = timer.elapsed; + async.elapse(oneSecond * 2); + expect(timer.isActive, isFalse); + }); + }); - // Wait for a couple more seconds, nothing should happen - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.paused, elapsed: elapsed); + test("should not expire after executing it's callback", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + async.elapse(oneSecond); + expect(timer.isActive, true); + }); + }); - // pause should do nothing either, even if more time passes - timer.pause(); - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.paused, elapsed: elapsed); + test("should call it's callback after the given duration", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); - // Resume the timer, it should be State.active again - timer.start(); - expectState(timer, oneSecond, State.active, elapsed: elapsed); + timer.start(); + async.elapse(oneSecond - Duration(microseconds: 1)); + expect(numCalls, 0); + expect(timer.isExpired, isFalse); - // Wait for the remaining time, then it should be expired - fakeTime.elapse(oneSecond - timer.elapsed); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + async.elapse(Duration(microseconds: 1)); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); + }); + }); - // Wait for a couple more seconds, nothing should happen - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + test("should call it's callback as many times as it's duration requires", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); - // pause when it's already expired should do nothing - timer.pause(); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + timer.start(); + async.elapse(oneSecond * 100); + expect(numCalls, 100); + }); }); }); - test('reset()', () { - fakeAsync((fakeTime) { - final timer = PausableTimer(oneSecond, callback); - - // resetting the timer upon start should be a NOP - timer.reset(); - expectState(timer, oneSecond, State.paused); - - // start and reset after half the time passed - timer.start(); - fakeTime.elapse(oneSecond ~/ 2); - expectState(timer, oneSecond, State.active, elapsed: oneSecond ~/ 2); - // reset should bring the elapsed time to zero-ish again but it should - // be still State.active - timer.reset(); - expectState(timer, oneSecond, State.active); - - // let some time pass again, pause and reset, it should be reset to - // nothing elapsed and still be paused - fakeTime.elapse(oneSecond ~/ 3); - timer.pause(); - fakeTime.elapse(oneSecond ~/ 3); - expectState(timer, oneSecond, State.paused, elapsed: oneSecond ~/ 3); - timer.reset(); - expectState(timer, oneSecond, State.paused); + group('Starting', () { + test("an active timer should do nothing", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + final initialState = timer.state; + + timer.start(); + final finalState = timer.state; + + expect(initialState, finalState); + }); }); - }); - group('cancel()', () { - test('just after creation', () { - fakeAsync((fakeTime) { - // cancel just after creation + test("a paused timer should resume it's timer", () { + fakeAsync((async) { final timer = PausableTimer(oneSecond, callback); - timer.cancel(); - expectState(timer, oneSecond, State.cancelled); - fakeTime.elapse(oneSecond * 2); - expectState(timer, oneSecond, State.cancelled); + + timer.start(); + async.elapse(halfSecond); + + timer.pause(); + expect(timer.elapsed, halfSecond); + + timer.start(); + expect(timer.elapsed, halfSecond); + + async.elapse(halfSecond); + expect(timer.isExpired, isTrue); + expect(numCalls, 1); + }); + }); + + test("an expired timer should do nothing", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + async.elapse(oneSecond); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); + + timer.start(); + expect(numCalls, 1); + expect(timer.isExpired, isTrue); + + async.elapse(oneSecond); + + expect(numCalls, 1); + expect(timer.isExpired, isTrue); }); }); - test('after start()', () { - fakeAsync((fakeTime) { - // cancel in the middle of the timer time + test("a cancelled timer should do nothing", () { + fakeAsync((async) { final timer = PausableTimer(oneSecond, callback); + timer.start(); - fakeTime.elapse(oneSecond ~/ 2); + async.elapse(halfSecond); timer.cancel(); - expectState(timer, oneSecond, State.cancelled, elapsed: oneSecond ~/ 2); - // calling cancel again should do nothing + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); + + timer.start(); + expect(timer.isCancelled, isTrue); + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); + }); + }); + + test("an active periodic timer should do nothing", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + final initialState = timer.state; + + timer.start(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test("a paused periodic timer should resume it's timer", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + async.elapse(halfSecond); + + timer.pause(); + expect(timer.elapsed, halfSecond); + + timer.start(); + expect(timer.elapsed, halfSecond); + + async.elapse(halfSecond); + expect(numCalls, 1); + }); + }); + + test("a cancelled periodic timer should do nothing", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + async.elapse(halfSecond); timer.cancel(); - expectState(timer, oneSecond, State.cancelled, elapsed: oneSecond ~/ 2); + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); + + timer.start(); + expect(timer.isCancelled, isTrue); + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); }); }); + }); - test('after pause()', () { - fakeAsync((fakeTime) { - // cancel after expiration should do nothing + group('Pausing', () { + test('an active timer should prevent the callback from executing', () { + fakeAsync((async) { final timer = PausableTimer(oneSecond, callback); + + timer.start(); + expect(timer.isActive, isTrue); + + async.elapse(halfSecond); + timer.pause(); + expect(timer.isPaused, isTrue); + + async.elapse(oneSecond); + expect(timer.isPaused, isTrue); + expect(numCalls, 0); + }); + }); + + test('a paused timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + final initialState = timer.state; + + async.elapse(oneSecond); + timer.pause(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test('an expired timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + async.elapse(oneSecond); + final initialState = timer.state; + + timer.pause(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test('a cancelled timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.cancel(); + final initialState = timer.state; + + async.elapse(oneSecond); + + timer.pause(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test('an active periodic timer should prevent the callback from executing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + timer.start(); - fakeTime.elapse(oneSecond); - expectState(timer, oneSecond, State.expired, elapsed: oneSecond, withCalls: 1); + expect(timer.isActive, isTrue); + + async.elapse(halfSecond); + timer.pause(); + expect(timer.isPaused, isTrue); + + async.elapse(oneSecond); + expect(timer.isPaused, isTrue); + expect(numCalls, 0); + }); + }); + + test('a paused periodic timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + final initialState = timer.state; + + async.elapse(oneSecond); + timer.pause(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test('a cancelled periodic timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + timer.cancel(); - expectState(timer, oneSecond, State.expiredCancelled, elapsed: oneSecond, withCalls: 1); + final initialState = timer.state; + + async.elapse(oneSecond); + + timer.pause(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + }); + + group('Resetting', () { + test("an active timer should revert it's timer to zero", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + async.elapse(halfSecond); + expect(timer.isActive, isTrue); + expect(timer.elapsed, halfSecond); + + timer.reset(); + expect(timer.isActive, isTrue); + expect(timer.elapsed, Duration.zero); + }); + }); + + test("a paused timer should revert it's timer to zero", () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + async.elapse(halfSecond); + timer.pause(); + expect(timer.isPaused, isTrue); + expect(timer.elapsed, halfSecond); + + timer.reset(); + expect(timer.isPaused, isTrue); + expect(timer.elapsed, Duration.zero); }); }); - test('first, then start(), pause() and reset() do nothing', () { - fakeAsync((fakeTime) { + test("an expired timer should revert it's timer to zero and it's status to paused", () { + fakeAsync((async) { final timer = PausableTimer(oneSecond, callback); + timer.start(); - fakeTime.elapse(oneSecond ~/ 2); + async.elapse(oneSecond); + expect(timer.isExpired, isTrue); + expect(numCalls, 1); + + timer.reset(); + expect(timer.isPaused, isTrue); + expect(timer.elapsed, Duration.zero); + }); + }); + + test('a cancelled timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + timer.cancel(); + final initialState = timer.state; + + timer.reset(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test("an active periodic timer should revert it's timer to zero", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + async.elapse(halfSecond); + expect(timer.isActive, isTrue); + expect(timer.elapsed, halfSecond); + + timer.reset(); + expect(timer.isActive, isTrue); + expect(timer.elapsed, Duration.zero); + }); + }); + + test("a paused periodic timer should revert it's timer to zero", () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); - // start(), pause() and reset() after cancel() do nothing timer.start(); - expectState(timer, oneSecond, State.cancelled, elapsed: oneSecond ~/ 2); + async.elapse(halfSecond); timer.pause(); - expectState(timer, oneSecond, State.cancelled, elapsed: oneSecond ~/ 2); + expect(timer.isPaused, isTrue); + expect(timer.elapsed, halfSecond); + + timer.reset(); + expect(timer.isPaused, isTrue); + expect(timer.elapsed, Duration.zero); + }); + }); + + test('a cancelled periodic timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.cancel(); + final initialState = timer.state; + timer.reset(); - expectState(timer, oneSecond, State.cancelled, elapsed: oneSecond ~/ 2); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + }); + + group('Cancelling', () { + test('an active timer should prevent the callback from executing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.start(); + expect(timer.isActive, isTrue); + + async.elapse(halfSecond); + timer.cancel(); + expect(timer.isCancelled, isTrue); + + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); + }); + }); + + test('a cancelled timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer(oneSecond, callback); + + timer.cancel(); + final initialState = timer.state; + + async.elapse(oneSecond); + + timer.cancel(); + final finalState = timer.state; + + expect(initialState, finalState); + }); + }); + + test('an active periodic timer should prevent the callback from executing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.start(); + expect(timer.isActive, isTrue); + + async.elapse(halfSecond); + timer.cancel(); + expect(timer.isCancelled, isTrue); + + async.elapse(oneSecond); + expect(timer.isCancelled, isTrue); + expect(numCalls, 0); + }); + }); + + test('a cancelled periodic timer should do nothing', () { + fakeAsync((async) { + final timer = PausableTimer.periodic(oneSecond, callback); + + timer.cancel(); + final initialState = timer.state; + + async.elapse(oneSecond); + + timer.cancel(); + final finalState = timer.state; + + expect(initialState, finalState); }); }); }); } -enum State { - active, - paused, - expired, - cancelled, - expiredCancelled; +extension on PausableTimer { + (bool, bool, bool, bool, Duration, Duration, int) get state => (isActive, isPaused, isExpired, isCancelled, duration, elapsed, tick); }