From ecd076e364f9388b4f7a3afc9d591c93ca321599 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 17 Dec 2024 01:33:06 +0900 Subject: [PATCH] Add unit tests --- lib/src/foundation/sheet_activity.dart | 2 +- lib/src/paged/paged_sheet_geometry.dart | 14 +- test/paged/paged_sheet_geometry_test.dart | 501 ++++++++++ .../paged_sheet_test.dart} | 0 test/src/stubbing.dart | 4 +- test/src/stubbing.mocks.dart | 869 ++++++++++++++++++ test/src/test_ticker.dart | 133 +++ test/src/test_ticker_test.dart | 145 +++ 8 files changed, 1661 insertions(+), 7 deletions(-) create mode 100644 test/paged/paged_sheet_geometry_test.dart rename test/{navigation/navigation_sheet_test.dart => paged/paged_sheet_test.dart} (100%) create mode 100644 test/src/test_ticker.dart create mode 100644 test/src/test_ticker_test.dart diff --git a/lib/src/foundation/sheet_activity.dart b/lib/src/foundation/sheet_activity.dart index 8e1f035..d09ebef 100644 --- a/lib/src/foundation/sheet_activity.dart +++ b/lib/src/foundation/sheet_activity.dart @@ -603,7 +603,7 @@ mixin UserControlledSheetActivityMixin } } -/// Appends the delta of the bottom viewport inset, which is typically +/// Appends the negative delta of the bottom viewport inset, which is typically /// equal to the height of the on-screen keyboard, to the [activityOwner]'s /// `pixels` to maintain the visual sheet position. @internal diff --git a/lib/src/paged/paged_sheet_geometry.dart b/lib/src/paged/paged_sheet_geometry.dart index 11331fc..6f84769 100644 --- a/lib/src/paged/paged_sheet_geometry.dart +++ b/lib/src/paged/paged_sheet_geometry.dart @@ -21,6 +21,9 @@ const kDefaultPagedSheetMinOffset = SheetAnchor.proportional(1); @internal const kDefaultPagedSheetMaxOffset = SheetAnchor.proportional(1); +@internal +const kDefaultPagedSheetTransitionCurve = Curves.easeInOutCubic; + class _RouteGeometry { Size? oldContentSize; Size? contentSize; @@ -157,7 +160,7 @@ class PagedSheetGeometry extends DraggableScrollableSheetPosition { assert(_routeGeometries.containsKey(originRoute)); assert(_routeGeometries.containsKey(destinationRoute)); _setCurrentRoute(null); - beginActivity(_PageTransitionSheetActivity( + beginActivity(RouteTransitionSheetActivity( originRouteOffset: () => _routeGeometries[originRoute]?.offset, destinationRouteOffset: () => _routeGeometries[destinationRoute]?.offset, animation: animation, @@ -179,7 +182,7 @@ class PagedSheetGeometry extends DraggableScrollableSheetPosition { originRoute: originRoute, destinationRoute: destinationRoute, animation: animation, - animationCurve: Curves.easeInOutCubic, + animationCurve: kDefaultPagedSheetTransitionCurve, ); case BackwardRouteTransition( @@ -191,7 +194,7 @@ class PagedSheetGeometry extends DraggableScrollableSheetPosition { originRoute: originRoute, destinationRoute: destinationRoute, animation: animation, - animationCurve: Curves.easeInOutCubic, + animationCurve: kDefaultPagedSheetTransitionCurve, ); case UserGestureRouteTransition( @@ -213,8 +216,9 @@ class PagedSheetGeometry extends DraggableScrollableSheetPosition { } } -class _PageTransitionSheetActivity extends SheetActivity { - _PageTransitionSheetActivity({ +@visibleForTesting +class RouteTransitionSheetActivity extends SheetActivity { + RouteTransitionSheetActivity({ required this.originRouteOffset, required this.destinationRouteOffset, required this.animation, diff --git a/test/paged/paged_sheet_geometry_test.dart b/test/paged/paged_sheet_geometry_test.dart new file mode 100644 index 0000000..bd9bb99 --- /dev/null +++ b/test/paged/paged_sheet_geometry_test.dart @@ -0,0 +1,501 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:smooth_sheets/src/foundation/sheet_activity.dart'; +import 'package:smooth_sheets/src/foundation/sheet_position.dart'; +import 'package:smooth_sheets/src/paged/paged_sheet_geometry.dart'; +import 'package:smooth_sheets/src/paged/paged_sheet_route.dart'; +import 'package:smooth_sheets/src/paged/route_transition_observer.dart'; + +import '../src/stubbing.dart'; +import '../src/stubbing.mocks.dart'; +import '../src/test_ticker.dart'; + +void main() { + // Required because AnimationController depends on SemanticsBinding. + TestWidgetsFlutterBinding.ensureInitialized(); + + late PagedSheetGeometry geometryUnderTest; + + setUp(() { + geometryUnderTest = PagedSheetGeometry( + context: MockSheetContext(), + ); + }); + + tearDown(() { + geometryUnderTest.dispose(); + }); + + group('Lifecycle test', () { + test('Before first build', () { + expect(geometryUnderTest.maybePixels, isNull); + expect(geometryUnderTest.maybeContentSize, isNull); + expect(geometryUnderTest.maybeViewportSize, isNull); + expect(geometryUnderTest.maybeViewportInsets, isNull); + expect(geometryUnderTest.activity, isA()); + }); + + test('First build', () { + _firstBuild( + geometry: geometryUnderTest, + viewportSize: const Size(400, 800), + initialRouteContentSize: const Size(400, 400), + initialOffset: const SheetAnchor.proportional(0.5), + initialMinOffset: const SheetAnchor.proportional(0.5), + initialMaxOffset: const SheetAnchor.proportional(1), + ); + + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeMinPixels, 200); + expect(geometryUnderTest.maybeMaxPixels, 400); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + expect(geometryUnderTest.maybeViewportSize, const Size(400, 800)); + expect(geometryUnderTest.maybeViewportInsets, EdgeInsets.zero); + expect(geometryUnderTest.activity, isA()); + }); + + test('Push a new route and then pop it', () { + final initialRoute = _firstBuild( + geometry: geometryUnderTest, + initialRouteContentSize: const Size(400, 400), + viewportSize: const Size(400, 800), + initialOffset: const SheetAnchor.proportional(0.5), + initialMinOffset: const SheetAnchor.proportional(0.5), + initialMaxOffset: const SheetAnchor.proportional(1), + ); + + final (newRoute, newRouteTransitionController) = _createRoute( + initialOffset: const SheetAnchor.proportional(1), + minOffset: const SheetAnchor.proportional(1), + maxOffset: const SheetAnchor.proportional(1), + transitionDuration: const Duration(milliseconds: 200), + ); + + final pushTransition = _startForwardTransition( + geometry: geometryUnderTest, + currentRoute: initialRoute, + newRoute: newRoute, + newRouteContentSize: const Size(400, 600), + newRouteTransitionController: newRouteTransitionController, + ); + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + expect(geometryUnderTest.activity, isA()); + + pushTransition + ..tickAndSettle() + ..end(); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + expect(geometryUnderTest.activity, isA()); + + final popTransition = _startBackwardTransition( + geometry: geometryUnderTest, + currentRoute: newRoute, + destinationRoute: initialRoute, + currentRouteTransitionController: newRouteTransitionController, + ); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + expect(geometryUnderTest.activity, isA()); + + popTransition + ..tickAndSettle() + ..end(); + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + expect(geometryUnderTest.activity, isA()); + }); + }); + + group('Transition test', () { + test('Animate offset when pushing a new route', () { + final initialRoute = _firstBuild( + geometry: geometryUnderTest, + initialRouteContentSize: const Size(400, 400), + viewportSize: const Size(400, 800), + initialOffset: const SheetAnchor.proportional(0.5), + initialMinOffset: const SheetAnchor.proportional(0.5), + initialMaxOffset: const SheetAnchor.proportional(1), + ); + + final (newRoute, newRouteTransitionController) = _createRoute( + initialOffset: const SheetAnchor.proportional(1), + minOffset: const SheetAnchor.proportional(1), + maxOffset: const SheetAnchor.proportional(1), + transitionDuration: const Duration(milliseconds: 200), + ); + + final pushTransition = _startForwardTransition( + geometry: geometryUnderTest, + currentRoute: initialRoute, + newRoute: newRoute, + newRouteContentSize: const Size(400, 600), + newRouteTransitionController: newRouteTransitionController, + ); + + pushTransition.tick(Duration.zero); + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + + const curve = kDefaultPagedSheetTransitionCurve; + pushTransition.tick(const Duration(milliseconds: 50)); + expect(geometryUnderTest.maybePixels, 400 * curve.transform(0.25) + 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + + pushTransition.tick(const Duration(milliseconds: 50)); + expect(geometryUnderTest.maybePixels, 400 * curve.transform(0.5) + 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + + pushTransition.tick(const Duration(milliseconds: 50)); + expect(geometryUnderTest.maybePixels, 400 * curve.transform(0.75) + 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + + pushTransition.tickAndSettle(); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + + pushTransition.end(); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + }); + + test('Animate offset when popping the current route', () { + final initialRoute = _firstBuild( + geometry: geometryUnderTest, + viewportSize: const Size(400, 800), + initialRouteContentSize: const Size(400, 400), + initialOffset: const SheetAnchor.proportional(0.5), + initialMinOffset: const SheetAnchor.proportional(0.5), + initialMaxOffset: const SheetAnchor.proportional(1), + ); + + final (newRoute, newRouteTransitionController) = _pushRoute( + geometry: geometryUnderTest, + currentRoute: initialRoute, + routeContentSize: const Size(400, 600), + initialOffset: const SheetAnchor.proportional(1), + minOffset: const SheetAnchor.proportional(1), + maxOffset: const SheetAnchor.proportional(1), + transitionDuration: const Duration(milliseconds: 200), + ); + + final popTransition = _startBackwardTransition( + geometry: geometryUnderTest, + currentRoute: newRoute, + destinationRoute: initialRoute, + currentRouteTransitionController: newRouteTransitionController, + ); + + popTransition.tick(Duration.zero); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + const curve = kDefaultPagedSheetTransitionCurve; + popTransition.tick(const Duration(milliseconds: 50)); + expect( + geometryUnderTest.maybePixels, + moreOrLessEquals(600 - 400 * curve.transform(0.25)), + ); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + popTransition.tick(const Duration(milliseconds: 50)); + expect( + geometryUnderTest.maybePixels, + moreOrLessEquals(600 - 400 * curve.transform(0.5)), + ); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + popTransition.tick(const Duration(milliseconds: 50)); + expect( + geometryUnderTest.maybePixels, + moreOrLessEquals(600 - 400 * curve.transform(0.75)), + ); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + popTransition.tickAndSettle(); + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + popTransition.end(); + expect(geometryUnderTest.maybePixels, 200); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + }); + + test('Maintain offsets of each route throughout the transitions', () {}); + + test('Sync offset with progress of the swipe-back gesture', () { + final initialRoute = _firstBuild( + geometry: geometryUnderTest, + viewportSize: const Size(400, 800), + initialRouteContentSize: const Size(400, 400), + initialOffset: const SheetAnchor.proportional(1), + initialMinOffset: const SheetAnchor.proportional(1), + initialMaxOffset: const SheetAnchor.proportional(1), + ); + + final (newRoute, newRouteTransitionController) = _pushRoute( + geometry: geometryUnderTest, + currentRoute: initialRoute, + routeContentSize: const Size(400, 600), + initialOffset: const SheetAnchor.proportional(1), + minOffset: const SheetAnchor.proportional(1), + maxOffset: const SheetAnchor.proportional(1), + transitionDuration: const Duration(milliseconds: 200), + ); + + final swipeBackGesture = _startUserGestureTransition( + geometry: geometryUnderTest, + currentRoute: newRoute, + previousRoute: initialRoute, + currentRouteTransitionController: newRouteTransitionController, + ); + expect(geometryUnderTest.maybePixels, 600); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + swipeBackGesture.dragTo(0.75); + expect(geometryUnderTest.maybePixels, 550); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + swipeBackGesture.dragTo(0.5); + expect(geometryUnderTest.maybePixels, 500); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + swipeBackGesture.dragTo(0.25); + expect(geometryUnderTest.maybePixels, 450); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + swipeBackGesture.releasePointer(); + expect(geometryUnderTest.maybePixels, 400); + expect(geometryUnderTest.maybeContentSize, const Size(400, 600)); + + swipeBackGesture.end(); + expect(geometryUnderTest.maybePixels, 400); + expect(geometryUnderTest.maybeContentSize, const Size(400, 400)); + }); + }); +} + +(MockBasePagedSheetRoute, TestAnimationController) _createRoute({ + required SheetAnchor initialOffset, + required SheetAnchor minOffset, + required SheetAnchor maxOffset, + required Duration transitionDuration, +}) { + final animationController = TestAnimationController( + value: 0, + duration: transitionDuration, + reverseDuration: transitionDuration, + ); + addTearDown(animationController.dispose); + + final route = MockBasePagedSheetRoute(); + when(route.initialOffset).thenReturn(initialOffset); + when(route.minOffset).thenReturn(minOffset); + when(route.maxOffset).thenReturn(maxOffset); + + return (route, animationController); +} + +MockBasePagedSheetRoute _firstBuild({ + required PagedSheetGeometry geometry, + required Size viewportSize, + required Size initialRouteContentSize, + required SheetAnchor initialOffset, + required SheetAnchor initialMinOffset, + required SheetAnchor initialMaxOffset, +}) { + final (initialRoute, _) = _createRoute( + initialOffset: initialOffset, + minOffset: initialMinOffset, + maxOffset: initialMaxOffset, + transitionDuration: Duration.zero, + ); + geometry + ..applyNewViewportInsets(EdgeInsets.zero) + ..applyNewViewportSize(viewportSize) + ..addRoute(initialRoute) + ..onTransition(NoRouteTransition(currentRoute: initialRoute)) + ..applyNewRouteContentSize(initialRoute, initialRouteContentSize) + ..finalizePosition(); + + return initialRoute; +} + +typedef _TransitionHandle = ({ + void Function(Duration) tick, + void Function() tickAndSettle, + VoidCallback end, +}); + +_TransitionHandle _startForwardTransition({ + required PagedSheetGeometry geometry, + required BasePagedSheetRoute currentRoute, + required BasePagedSheetRoute newRoute, + required Size newRouteContentSize, + required TestAnimationController newRouteTransitionController, +}) { + newRouteTransitionController.forward(); + geometry + ..addRoute(newRoute) + ..onTransition( + ForwardRouteTransition( + originRoute: currentRoute, + destinationRoute: newRoute, + animation: newRouteTransitionController, + ), + ) + ..applyNewRouteContentSize(newRoute, newRouteContentSize) + ..finalizePosition(); + + return ( + tick: (duration) { + newRouteTransitionController.tick(duration); + geometry.finalizePosition(); + }, + tickAndSettle: () { + newRouteTransitionController.tickAndSettle(); + geometry.finalizePosition(); + }, + end: () { + geometry + ..onTransition(NoRouteTransition(currentRoute: newRoute)) + ..finalizePosition(); + }, + ); +} + +_TransitionHandle _startBackwardTransition({ + required PagedSheetGeometry geometry, + required BasePagedSheetRoute currentRoute, + required BasePagedSheetRoute destinationRoute, + required TestAnimationController currentRouteTransitionController, +}) { + currentRouteTransitionController.reverse(); + geometry + ..onTransition( + ForwardRouteTransition( + originRoute: currentRoute, + destinationRoute: destinationRoute, + animation: currentRouteTransitionController.drive( + Tween(begin: 1, end: 0), + ), + ), + ) + ..finalizePosition(); + + return ( + tick: (duration) { + currentRouteTransitionController.tick(duration); + geometry.finalizePosition(); + }, + tickAndSettle: () { + currentRouteTransitionController.tickAndSettle(); + geometry.finalizePosition(); + }, + end: () { + geometry + ..onTransition(NoRouteTransition(currentRoute: destinationRoute)) + ..removeRoute(currentRoute) + ..finalizePosition(); + }, + ); +} + +typedef _UserGestureTransitionHandle = ({ + ValueSetter dragTo, + VoidCallback releasePointer, + VoidCallback end, +}); + +_UserGestureTransitionHandle _startUserGestureTransition({ + required PagedSheetGeometry geometry, + required BasePagedSheetRoute currentRoute, + required BasePagedSheetRoute previousRoute, + required TestAnimationController currentRouteTransitionController, +}) { + currentRouteTransitionController.value = 1; + geometry + ..onTransition( + UserGestureRouteTransition( + currentRoute: currentRoute, + previousRoute: previousRoute, + animation: currentRouteTransitionController.drive( + Tween(begin: 1, end: 0), + ), + ), + ) + ..finalizePosition(); + + return ( + dragTo: (progress) { + currentRouteTransitionController.value = progress; + geometry.finalizePosition(); + }, + releasePointer: () { + if (currentRouteTransitionController.value < 0.5) { + currentRouteTransitionController.reverse(); + } else { + currentRouteTransitionController.forward(); + } + currentRouteTransitionController.tickAndSettle(); + geometry.finalizePosition(); + }, + end: () { + geometry + ..onTransition( + NoRouteTransition( + currentRoute: currentRouteTransitionController.isDismissed + ? previousRoute + : currentRoute, + ), + ) + ..finalizePosition(); + }, + ); +} + +(MockBasePagedSheetRoute, TestAnimationController) _pushRoute({ + required PagedSheetGeometry geometry, + required BasePagedSheetRoute currentRoute, + required Size routeContentSize, + required SheetAnchor initialOffset, + required SheetAnchor minOffset, + required SheetAnchor maxOffset, + required Duration transitionDuration, +}) { + final (newRoute, controller) = _createRoute( + initialOffset: initialOffset, + minOffset: minOffset, + maxOffset: maxOffset, + transitionDuration: transitionDuration, + ); + _startForwardTransition( + geometry: geometry, + currentRoute: currentRoute, + newRoute: newRoute, + newRouteContentSize: routeContentSize, + newRouteTransitionController: controller, + ) + ..tickAndSettle() + ..end(); + + return (newRoute, controller); +} + +void _popRoute({ + required PagedSheetGeometry geometry, + required BasePagedSheetRoute currentRoute, + required BasePagedSheetRoute destinationRoute, + required TestAnimationController currentRouteTransitionController, +}) { + _startBackwardTransition( + geometry: geometry, + currentRoute: currentRoute, + destinationRoute: destinationRoute, + currentRouteTransitionController: currentRouteTransitionController, + ) + ..tickAndSettle() + ..end(); +} diff --git a/test/navigation/navigation_sheet_test.dart b/test/paged/paged_sheet_test.dart similarity index 100% rename from test/navigation/navigation_sheet_test.dart rename to test/paged/paged_sheet_test.dart diff --git a/test/src/stubbing.dart b/test/src/stubbing.dart index 828c8bb..c7e3e31 100644 --- a/test/src/stubbing.dart +++ b/test/src/stubbing.dart @@ -5,6 +5,7 @@ import 'package:mockito/mockito.dart'; import 'package:smooth_sheets/src/foundation/foundation.dart'; import 'package:smooth_sheets/src/foundation/sheet_context.dart'; import 'package:smooth_sheets/src/foundation/sheet_position.dart'; +import 'package:smooth_sheets/src/paged/paged_sheet_route.dart'; @GenerateNiceMocks([ MockSpec(), @@ -12,7 +13,8 @@ import 'package:smooth_sheets/src/foundation/sheet_position.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec() + MockSpec(), + MockSpec(), ]) import 'stubbing.mocks.dart'; diff --git a/test/src/stubbing.mocks.dart b/test/src/stubbing.mocks.dart index e404d94..3d40b4b 100644 --- a/test/src/stubbing.mocks.dart +++ b/test/src/stubbing.mocks.dart @@ -21,6 +21,7 @@ import 'package:smooth_sheets/src/foundation/sheet_gesture_tamperer.dart' import 'package:smooth_sheets/src/foundation/sheet_physics.dart' as _i3; import 'package:smooth_sheets/src/foundation/sheet_position.dart' as _i4; import 'package:smooth_sheets/src/foundation/sheet_status.dart' as _i13; +import 'package:smooth_sheets/src/paged/paged_sheet_route.dart' as _i17; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -188,6 +189,72 @@ class _FakeTicker_13 extends _i1.SmartFake implements _i11.Ticker { String toString({bool? debugIncludeStack = false}) => super.toString(); } +class _FakeCurve_14 extends _i1.SmartFake implements _i7.Curve { + _FakeCurve_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDuration_15 extends _i1.SmartFake implements Duration { + _FakeDuration_15( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRouteSettings_16 extends _i1.SmartFake implements _i7.RouteSettings { + _FakeRouteSettings_16( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeValueListenable_17 extends _i1.SmartFake + implements _i10.ValueListenable { + _FakeValueListenable_17( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_18 extends _i1.SmartFake implements _i7.Widget { + _FakeWidget_18( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i7.DiagnosticLevel? minLevel = _i7.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeAnimationController_19 extends _i1.SmartFake + implements _i7.AnimationController { + _FakeAnimationController_19( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [SheetPosition]. /// /// See the documentation for Mockito's code generation for more information. @@ -482,6 +549,24 @@ class MockSheetPosition extends _i1.Mock implements _i4.SheetPosition { returnValueForMissingStub: null, ); + @override + void onFinalizePosition( + _i6.Size? oldContentSize, + _i6.Size? oldViewportSize, + _i7.EdgeInsets? oldViewportInsets, + ) => + super.noSuchMethod( + Invocation.method( + #onFinalizePosition, + [ + oldContentSize, + oldViewportSize, + oldViewportInsets, + ], + ), + returnValueForMissingStub: null, + ); + @override void beginActivity(_i5.SheetActivity<_i4.SheetPosition>? activity) => super.noSuchMethod( @@ -1595,3 +1680,787 @@ class MockTickerProvider extends _i1.Mock implements _i7.TickerProvider { ), ) as _i11.Ticker); } + +/// A class which mocks [BasePagedSheetRoute]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBasePagedSheetRoute extends _i1.Mock + implements _i17.BasePagedSheetRoute { + @override + _i4.SheetAnchor get initialOffset => (super.noSuchMethod( + Invocation.getter(#initialOffset), + returnValue: _FakeSheetAnchor_4( + this, + Invocation.getter(#initialOffset), + ), + returnValueForMissingStub: _FakeSheetAnchor_4( + this, + Invocation.getter(#initialOffset), + ), + ) as _i4.SheetAnchor); + + @override + _i4.SheetAnchor get minOffset => (super.noSuchMethod( + Invocation.getter(#minOffset), + returnValue: _FakeSheetAnchor_4( + this, + Invocation.getter(#minOffset), + ), + returnValueForMissingStub: _FakeSheetAnchor_4( + this, + Invocation.getter(#minOffset), + ), + ) as _i4.SheetAnchor); + + @override + _i4.SheetAnchor get maxOffset => (super.noSuchMethod( + Invocation.getter(#maxOffset), + returnValue: _FakeSheetAnchor_4( + this, + Invocation.getter(#maxOffset), + ), + returnValueForMissingStub: _FakeSheetAnchor_4( + this, + Invocation.getter(#maxOffset), + ), + ) as _i4.SheetAnchor); + + @override + _i3.SheetPhysics get physics => (super.noSuchMethod( + Invocation.getter(#physics), + returnValue: _FakeSheetPhysics_1( + this, + Invocation.getter(#physics), + ), + returnValueForMissingStub: _FakeSheetPhysics_1( + this, + Invocation.getter(#physics), + ), + ) as _i3.SheetPhysics); + + @override + bool get fullscreenDialog => (super.noSuchMethod( + Invocation.getter(#fullscreenDialog), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get allowSnapshotting => (super.noSuchMethod( + Invocation.getter(#allowSnapshotting), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get opaque => (super.noSuchMethod( + Invocation.getter(#opaque), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get barrierDismissible => (super.noSuchMethod( + Invocation.getter(#barrierDismissible), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get popGestureEnabled => (super.noSuchMethod( + Invocation.getter(#popGestureEnabled), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get semanticsDismissible => (super.noSuchMethod( + Invocation.getter(#semanticsDismissible), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i7.Curve get barrierCurve => (super.noSuchMethod( + Invocation.getter(#barrierCurve), + returnValue: _FakeCurve_14( + this, + Invocation.getter(#barrierCurve), + ), + returnValueForMissingStub: _FakeCurve_14( + this, + Invocation.getter(#barrierCurve), + ), + ) as _i7.Curve); + + @override + bool get maintainState => (super.noSuchMethod( + Invocation.getter(#maintainState), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get popGestureInProgress => (super.noSuchMethod( + Invocation.getter(#popGestureInProgress), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get offstage => (super.noSuchMethod( + Invocation.getter(#offstage), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + set offstage(bool? value) => super.noSuchMethod( + Invocation.setter( + #offstage, + value, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.RoutePopDisposition get popDisposition => (super.noSuchMethod( + Invocation.getter(#popDisposition), + returnValue: _i7.RoutePopDisposition.pop, + returnValueForMissingStub: _i7.RoutePopDisposition.pop, + ) as _i7.RoutePopDisposition); + + @override + bool get hasScopedWillPopCallback => (super.noSuchMethod( + Invocation.getter(#hasScopedWillPopCallback), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get canPop => (super.noSuchMethod( + Invocation.getter(#canPop), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get impliesAppBarDismissal => (super.noSuchMethod( + Invocation.getter(#impliesAppBarDismissal), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get willDisposeAnimationController => (super.noSuchMethod( + Invocation.getter(#willDisposeAnimationController), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + set willDisposeAnimationController(bool? _willDisposeAnimationController) => + super.noSuchMethod( + Invocation.setter( + #willDisposeAnimationController, + _willDisposeAnimationController, + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Future get completed => (super.noSuchMethod( + Invocation.getter(#completed), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + Duration get transitionDuration => (super.noSuchMethod( + Invocation.getter(#transitionDuration), + returnValue: _FakeDuration_15( + this, + Invocation.getter(#transitionDuration), + ), + returnValueForMissingStub: _FakeDuration_15( + this, + Invocation.getter(#transitionDuration), + ), + ) as Duration); + + @override + Duration get reverseTransitionDuration => (super.noSuchMethod( + Invocation.getter(#reverseTransitionDuration), + returnValue: _FakeDuration_15( + this, + Invocation.getter(#reverseTransitionDuration), + ), + returnValueForMissingStub: _FakeDuration_15( + this, + Invocation.getter(#reverseTransitionDuration), + ), + ) as Duration); + + @override + bool get finishedWhenPopped => (super.noSuchMethod( + Invocation.getter(#finishedWhenPopped), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + String get debugLabel => (super.noSuchMethod( + Invocation.getter(#debugLabel), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#debugLabel), + ), + returnValueForMissingStub: _i16.dummyValue( + this, + Invocation.getter(#debugLabel), + ), + ) as String); + + @override + List<_i7.OverlayEntry> get overlayEntries => (super.noSuchMethod( + Invocation.getter(#overlayEntries), + returnValue: <_i7.OverlayEntry>[], + returnValueForMissingStub: <_i7.OverlayEntry>[], + ) as List<_i7.OverlayEntry>); + + @override + _i7.RouteSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeRouteSettings_16( + this, + Invocation.getter(#settings), + ), + returnValueForMissingStub: _FakeRouteSettings_16( + this, + Invocation.getter(#settings), + ), + ) as _i7.RouteSettings); + + @override + _i10.ValueListenable get restorationScopeId => (super.noSuchMethod( + Invocation.getter(#restorationScopeId), + returnValue: _FakeValueListenable_17( + this, + Invocation.getter(#restorationScopeId), + ), + returnValueForMissingStub: _FakeValueListenable_17( + this, + Invocation.getter(#restorationScopeId), + ), + ) as _i10.ValueListenable); + + @override + bool get willHandlePopInternally => (super.noSuchMethod( + Invocation.getter(#willHandlePopInternally), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i9.Future get popped => (super.noSuchMethod( + Invocation.getter(#popped), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + bool get isCurrent => (super.noSuchMethod( + Invocation.getter(#isCurrent), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get isFirst => (super.noSuchMethod( + Invocation.getter(#isFirst), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get hasActiveRouteBelow => (super.noSuchMethod( + Invocation.getter(#hasActiveRouteBelow), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool get isActive => (super.noSuchMethod( + Invocation.getter(#isActive), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + void install() => super.noSuchMethod( + Invocation.method( + #install, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + bool canTransitionFrom(_i7.TransitionRoute? previousRoute) => + (super.noSuchMethod( + Invocation.method( + #canTransitionFrom, + [previousRoute], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + bool canTransitionTo(_i7.TransitionRoute? nextRoute) => + (super.noSuchMethod( + Invocation.method( + #canTransitionTo, + [nextRoute], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i7.Widget buildContent( + _i7.BuildContext? context, + _i7.Animation? animation, + _i7.Animation? secondaryAnimation, + ) => + (super.noSuchMethod( + Invocation.method( + #buildContent, + [ + context, + animation, + secondaryAnimation, + ], + ), + returnValue: _FakeWidget_18( + this, + Invocation.method( + #buildContent, + [ + context, + animation, + secondaryAnimation, + ], + ), + ), + returnValueForMissingStub: _FakeWidget_18( + this, + Invocation.method( + #buildContent, + [ + context, + animation, + secondaryAnimation, + ], + ), + ), + ) as _i7.Widget); + + @override + _i7.Widget buildPage( + _i7.BuildContext? context, + _i7.Animation? animation, + _i7.Animation? secondaryAnimation, + ) => + (super.noSuchMethod( + Invocation.method( + #buildPage, + [ + context, + animation, + secondaryAnimation, + ], + ), + returnValue: _FakeWidget_18( + this, + Invocation.method( + #buildPage, + [ + context, + animation, + secondaryAnimation, + ], + ), + ), + returnValueForMissingStub: _FakeWidget_18( + this, + Invocation.method( + #buildPage, + [ + context, + animation, + secondaryAnimation, + ], + ), + ), + ) as _i7.Widget); + + @override + _i7.Widget buildTransitions( + _i7.BuildContext? context, + _i7.Animation? animation, + _i7.Animation? secondaryAnimation, + _i7.Widget? child, + ) => + (super.noSuchMethod( + Invocation.method( + #buildTransitions, + [ + context, + animation, + secondaryAnimation, + child, + ], + ), + returnValue: _FakeWidget_18( + this, + Invocation.method( + #buildTransitions, + [ + context, + animation, + secondaryAnimation, + child, + ], + ), + ), + returnValueForMissingStub: _FakeWidget_18( + this, + Invocation.method( + #buildTransitions, + [ + context, + animation, + secondaryAnimation, + child, + ], + ), + ), + ) as _i7.Widget); + + @override + void setState(_i6.VoidCallback? fn) => super.noSuchMethod( + Invocation.method( + #setState, + [fn], + ), + returnValueForMissingStub: null, + ); + + @override + _i7.TickerFuture didPush() => (super.noSuchMethod( + Invocation.method( + #didPush, + [], + ), + returnValue: _FakeTickerFuture_10( + this, + Invocation.method( + #didPush, + [], + ), + ), + returnValueForMissingStub: _FakeTickerFuture_10( + this, + Invocation.method( + #didPush, + [], + ), + ), + ) as _i7.TickerFuture); + + @override + void didAdd() => super.noSuchMethod( + Invocation.method( + #didAdd, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Future<_i7.RoutePopDisposition> willPop() => (super.noSuchMethod( + Invocation.method( + #willPop, + [], + ), + returnValue: _i9.Future<_i7.RoutePopDisposition>.value( + _i7.RoutePopDisposition.pop), + returnValueForMissingStub: _i9.Future<_i7.RoutePopDisposition>.value( + _i7.RoutePopDisposition.pop), + ) as _i9.Future<_i7.RoutePopDisposition>); + + @override + void onPopInvoked(bool? didPop) => super.noSuchMethod( + Invocation.method( + #onPopInvoked, + [didPop], + ), + returnValueForMissingStub: null, + ); + + @override + void addScopedWillPopCallback(_i7.WillPopCallback? callback) => + super.noSuchMethod( + Invocation.method( + #addScopedWillPopCallback, + [callback], + ), + returnValueForMissingStub: null, + ); + + @override + void removeScopedWillPopCallback(_i7.WillPopCallback? callback) => + super.noSuchMethod( + Invocation.method( + #removeScopedWillPopCallback, + [callback], + ), + returnValueForMissingStub: null, + ); + + @override + void registerPopEntry(_i7.PopEntry? popEntry) => super.noSuchMethod( + Invocation.method( + #registerPopEntry, + [popEntry], + ), + returnValueForMissingStub: null, + ); + + @override + void unregisterPopEntry(_i7.PopEntry? popEntry) => super.noSuchMethod( + Invocation.method( + #unregisterPopEntry, + [popEntry], + ), + returnValueForMissingStub: null, + ); + + @override + void didChangePrevious(_i7.Route? previousRoute) => + super.noSuchMethod( + Invocation.method( + #didChangePrevious, + [previousRoute], + ), + returnValueForMissingStub: null, + ); + + @override + void didChangeNext(_i7.Route? nextRoute) => super.noSuchMethod( + Invocation.method( + #didChangeNext, + [nextRoute], + ), + returnValueForMissingStub: null, + ); + + @override + void didPopNext(_i7.Route? nextRoute) => super.noSuchMethod( + Invocation.method( + #didPopNext, + [nextRoute], + ), + returnValueForMissingStub: null, + ); + + @override + void changedInternalState() => super.noSuchMethod( + Invocation.method( + #changedInternalState, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void changedExternalState() => super.noSuchMethod( + Invocation.method( + #changedExternalState, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Widget buildModalBarrier() => (super.noSuchMethod( + Invocation.method( + #buildModalBarrier, + [], + ), + returnValue: _FakeWidget_18( + this, + Invocation.method( + #buildModalBarrier, + [], + ), + ), + returnValueForMissingStub: _FakeWidget_18( + this, + Invocation.method( + #buildModalBarrier, + [], + ), + ), + ) as _i7.Widget); + + @override + Iterable<_i7.OverlayEntry> createOverlayEntries() => (super.noSuchMethod( + Invocation.method( + #createOverlayEntries, + [], + ), + returnValue: <_i7.OverlayEntry>[], + returnValueForMissingStub: <_i7.OverlayEntry>[], + ) as Iterable<_i7.OverlayEntry>); + + @override + _i7.AnimationController createAnimationController() => (super.noSuchMethod( + Invocation.method( + #createAnimationController, + [], + ), + returnValue: _FakeAnimationController_19( + this, + Invocation.method( + #createAnimationController, + [], + ), + ), + returnValueForMissingStub: _FakeAnimationController_19( + this, + Invocation.method( + #createAnimationController, + [], + ), + ), + ) as _i7.AnimationController); + + @override + _i7.Animation createAnimation() => (super.noSuchMethod( + Invocation.method( + #createAnimation, + [], + ), + returnValue: _FakeAnimation_9( + this, + Invocation.method( + #createAnimation, + [], + ), + ), + returnValueForMissingStub: _FakeAnimation_9( + this, + Invocation.method( + #createAnimation, + [], + ), + ), + ) as _i7.Animation); + + @override + void didReplace(_i7.Route? oldRoute) => super.noSuchMethod( + Invocation.method( + #didReplace, + [oldRoute], + ), + returnValueForMissingStub: null, + ); + + @override + bool didPop(T? result) => (super.noSuchMethod( + Invocation.method( + #didPop, + [result], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + void handleStartBackGesture({double? progress = 0.0}) => super.noSuchMethod( + Invocation.method( + #handleStartBackGesture, + [], + {#progress: progress}, + ), + returnValueForMissingStub: null, + ); + + @override + void handleUpdateBackGestureProgress({required double? progress}) => + super.noSuchMethod( + Invocation.method( + #handleUpdateBackGestureProgress, + [], + {#progress: progress}, + ), + returnValueForMissingStub: null, + ); + + @override + void handleCancelBackGesture() => super.noSuchMethod( + Invocation.method( + #handleCancelBackGesture, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void handleCommitBackGesture() => super.noSuchMethod( + Invocation.method( + #handleCommitBackGesture, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void didComplete(T? result) => super.noSuchMethod( + Invocation.method( + #didComplete, + [result], + ), + returnValueForMissingStub: null, + ); + + @override + void addLocalHistoryEntry(_i7.LocalHistoryEntry? entry) => super.noSuchMethod( + Invocation.method( + #addLocalHistoryEntry, + [entry], + ), + returnValueForMissingStub: null, + ); + + @override + void removeLocalHistoryEntry(_i7.LocalHistoryEntry? entry) => + super.noSuchMethod( + Invocation.method( + #removeLocalHistoryEntry, + [entry], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/src/test_ticker.dart b/test/src/test_ticker.dart new file mode 100644 index 0000000..cc6e9f5 --- /dev/null +++ b/test/src/test_ticker.dart @@ -0,0 +1,133 @@ +import 'package:flutter/animation.dart'; +import 'package:flutter/scheduler.dart'; + +import 'stubbing.dart'; + +class TestTicker implements Ticker { + TestTicker(this._onTick, {this.debugLabel}); + + final TickerCallback _onTick; + + @override + final String? debugLabel; + + Duration _elapsed = Duration.zero; + MockTickerFuture? _currentFuture; + + @override + bool get isActive => _currentFuture != null; + + @override + bool get isTicking => isActive && !muted; + + @override + bool muted = false; + + /// Manually advances the ticker by the specified duration. + void tick(Duration duration) { + if (isActive && !muted) { + _elapsed += duration; + _onTick(_elapsed); + } + } + + /// Calls [tick] repeatedly with given [duration] until the ticker is + /// no longer active. + void tickAndSettle({Duration duration = const Duration(milliseconds: 100)}) { + while (isActive) { + tick(duration); + } + } + + @override + TickerFuture start() { + if (isActive) { + throw StateError('MockTicker cannot be started while already active.'); + } + _elapsed = Duration.zero; + return _currentFuture = MockTickerFuture(); + } + + @override + void stop({bool canceled = false}) { + if (!isActive) return; + _currentFuture = null; + } + + @override + void dispose() { + stop(canceled: true); + } + + @override + String toString({bool debugIncludeStack = false}) { + return 'MockTicker(${debugLabel ?? ''})'; + } + + @override + void noSuchMethod(Invocation invocation) { + throw UnimplementedError(); + } +} + +class _SingleVsync implements TickerProvider { + _SingleVsync(); + + TestTicker? _ticker; + + @override + Ticker createTicker(TickerCallback onTick) { + assert(_ticker == null); + return _ticker = TestTicker(onTick); + } + + void dispose() { + _ticker?.dispose(); + } +} + +class TestAnimationController extends AnimationController { + factory TestAnimationController({ + String? debugLabel, + double? value, + Duration? duration, + Duration? reverseDuration, + double lowerBound = 0, + double upperBound = 1, + }) { + final vsync = _SingleVsync(); + return TestAnimationController._( + debugLabel: debugLabel, + value: value, + duration: duration, + reverseDuration: reverseDuration, + lowerBound: lowerBound, + upperBound: upperBound, + vsync: vsync, + ); + } + + TestAnimationController._({ + required super.debugLabel, + required super.value, + required super.duration, + required super.reverseDuration, + required super.lowerBound, + required super.upperBound, + required _SingleVsync vsync, + }) : _vsync = vsync, + super(vsync: vsync); + + final _SingleVsync _vsync; + + void tick(Duration duration) => _vsync._ticker?.tick(duration); + + void tickAndSettle({Duration duration = const Duration(milliseconds: 100)}) => + _vsync._ticker?.tickAndSettle(duration: duration); + + @override + void dispose() { + _vsync.dispose(); + super.dispose(); + } +} diff --git a/test/src/test_ticker_test.dart b/test/src/test_ticker_test.dart new file mode 100644 index 0000000..1e9a7d3 --- /dev/null +++ b/test/src/test_ticker_test.dart @@ -0,0 +1,145 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'test_ticker.dart'; + +void main() { + test('Should call onTick callback when tick is manually advanced', () { + final ticks = []; + final mockTicker = TestTicker(ticks.add); + + mockTicker.start(); + mockTicker.tick(const Duration(milliseconds: 500)); + mockTicker.tick(const Duration(milliseconds: 500)); + + expect(ticks, const [ + Duration(milliseconds: 500), + Duration(milliseconds: 1000), + ]); + }); + + test('Should reset elapsed time when start is called', () { + final ticks = []; + final mockTicker = TestTicker(ticks.add); + + mockTicker.start(); + mockTicker.tick(const Duration(milliseconds: 500)); + mockTicker.stop(); + mockTicker.start(); // Reset + mockTicker.tick(const Duration(milliseconds: 300)); + + expect(ticks, const [ + Duration(milliseconds: 500), + Duration(milliseconds: 300), + ]); + }); + + test('Should not call onTick when ticker is stopped', () { + final ticks = []; + final mockTicker = TestTicker(ticks.add); + + mockTicker.start(); + mockTicker.tick(const Duration(milliseconds: 500)); + mockTicker.stop(); + mockTicker.tick(const Duration(milliseconds: 500)); + + expect(ticks, const [Duration(milliseconds: 500)]); + }); + + test('Should not call onTick when ticker is muted', () { + final ticks = []; + final mockTicker = TestTicker(ticks.add); + + mockTicker.start(); + mockTicker.muted = true; + mockTicker.tick(const Duration(milliseconds: 500)); + + expect(ticks, isEmpty); + }); + + test('Should throw error if start is called while already active', () { + final mockTicker = TestTicker((elapsed) {}); + + mockTicker.start(); + expect(mockTicker.start, throwsA(isA())); + }); + + test('Should correctly reflect isActive state', () { + final mockTicker = TestTicker((elapsed) {}); + + expect(mockTicker.isActive, isFalse); + + mockTicker.start(); + expect(mockTicker.isActive, isTrue); + + mockTicker.stop(); + expect(mockTicker.isActive, isFalse); + }); + + test('Should correctly reflect isTicking state', () { + final mockTicker = TestTicker((elapsed) {}); + + mockTicker.start(); + expect(mockTicker.isTicking, isTrue); + + mockTicker.muted = true; + expect(mockTicker.isTicking, isFalse); + + mockTicker.muted = false; + expect(mockTicker.isTicking, isTrue); + }); + + test('dispose Should stop ticker and make it inactive', () { + final mockTicker = TestTicker((elapsed) {}); + + mockTicker.start(); + mockTicker.dispose(); + + expect(mockTicker.isActive, isFalse); + expect(mockTicker.isTicking, isFalse); + }); + + test('tick Should not advance if ticker is disposed', () { + final ticks = []; + final mockTicker = TestTicker(ticks.add); + + mockTicker.start(); + mockTicker.dispose(); + mockTicker.tick(const Duration(milliseconds: 500)); + + expect(ticks, isEmpty); + }); + + test( + 'should call onTick repeatedly with custom duration ' + 'until ticker is inactive', () { + late TestTicker ticker; + final tickDurations = []; + void onTick(Duration elapsed) { + tickDurations.add(elapsed); + if (elapsed >= const Duration(milliseconds: 500)) { + ticker.stop(); + } + } + + ticker = TestTicker(onTick); + + ticker.start(); + ticker.tickAndSettle(duration: const Duration(milliseconds: 120)); + + expect( + tickDurations, + const [ + Duration(milliseconds: 120), + Duration(milliseconds: 240), + Duration(milliseconds: 360), + Duration(milliseconds: 480), + Duration(milliseconds: 600), + ], + ); + }); + + test('toString Should include debugLabel if provided', () { + final mockTicker = TestTicker((elapsed) {}, debugLabel: 'debugLabel'); + expect(mockTicker.toString(), contains('debugLabel')); + }); +}