diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d444c..d565ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.1 + +* **Fix (Transitions & Pop Visibility)**: Prevented underlying screen from disappearing or flashing black during push and pop route transitions by eliminating destructive secondary opacity fading in `Scale`, `FadeScale`, and `SharedAxis` widgets. +* **Feat (Transitions)**: Added optional `fillColor` property support in `FadeThrough` transition widget. +* **Test**: Added integration test suite verifying that underlying screens remain visible and mounted throughout transition lifecycle during push and pop operations. + ## 0.3.0 * **Feat (Transitions & Motion)**: Implemented modern **Material 3 Transitions** (`fade_through`, `fade_scale`, `shared_axis_x`, `shared_axis_y`, `shared_axis_z`, `scale`) and authentic **Cupertino iOS 16–18** transition (`slide_cupertino` with flat edges, 1/3 horizontal parallax, and left-edge drop shadow). diff --git a/example/lib/app_traces.dart b/example/lib/app_traces.dart index 3e885e7..31e6c6f 100644 --- a/example/lib/app_traces.dart +++ b/example/lib/app_traces.dart @@ -23,11 +23,7 @@ enum AppPath { /// event, bundling the path, arguments, and presentation (`TraceRoute`). class AppTrace extends Trace { /// Private constructor to be used by the factory constructors. - const new _({ - required super.path, - super.args, - super.traceRoute, - }); + const new _({required super.path, super.args, super.traceRoute}); /// Navigates to the push arguments screen. factory toPushArgs({ @@ -86,27 +82,26 @@ class AppTrace extends Trace { source: 'CustomBuildTransition', ), traceRoute: PageTraceRoute( - customTransition: CustomBuildTransition( - ({ - required animation, - required secondaryAnimation, - required child, - }) { - final curvedAnimation = CurvedAnimation( - parent: animation, - curve: Curves.easeOutBack, - ); - return ScaleTransition( - scale: curvedAnimation, - child: RotationTransition( - turns: Tween(begin: 0.85, end: 1).animate( - curvedAnimation, - ), - child: child, - ), - ); - }, - ), + customTransition: CustomBuildTransition(({ + required animation, + required secondaryAnimation, + required child, + }) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeOutBack, + ); + return ScaleTransition( + scale: curvedAnimation, + child: RotationTransition( + turns: Tween( + begin: 0.85, + end: 1, + ).animate(curvedAnimation), + child: child, + ), + ); + }), ), ); } diff --git a/example/lib/detail_args.dart b/example/lib/detail_args.dart index affc8ac..3bbec7c 100644 --- a/example/lib/detail_args.dart +++ b/example/lib/detail_args.dart @@ -1,9 +1,7 @@ import 'package:browser_router/browser.dart'; final class DetailArgs extends RouteParams { - new({ - required this.message, - }); + new({required this.message}); final String message; diff --git a/example/lib/home_screen.dart b/example/lib/home_screen.dart index c74f80b..9bb2053 100644 --- a/example/lib/home_screen.dart +++ b/example/lib/home_screen.dart @@ -72,7 +72,9 @@ class _HomeScreenState extends State { child: ListView( children: [ // --- Material 3 Motion --- - const _SectionTitle(title: '✨ Material 3 Motion Transitions'), + const _SectionTitle( + title: '✨ Material 3 Motion Transitions', + ), _ExampleButton( text: 'Fade Through (Top-Level Tabs / Bar)', onTap: () => AppTrace.toPushArgs( @@ -116,7 +118,9 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Apple Cupertino iOS Motion --- - const _SectionTitle(title: '🍎 iOS 16–18 Cupertino Native Motion'), + const _SectionTitle( + title: '🍎 iOS 16–18 Cupertino Native Motion', + ), _ExampleButton( text: 'Slide Cupertino (Shadow + 1/3 Parallax)', onTap: () => AppTrace.toPushArgs( @@ -128,11 +132,14 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Web Micro-Transitions --- - const _SectionTitle(title: '🌐 Web Micro-Transitions (SPA)'), + const _SectionTitle( + title: '🌐 Web Micro-Transitions (SPA)', + ), _ExampleButton( text: 'Fade Scale (Fast Zoom-Fade 0.95->1.0)', onTap: () => AppTrace.toPushArgs( - message: 'Snappy SPA Zoom-Fade (0.95->1.0) with fastEasing', + message: + 'Snappy SPA Zoom-Fade (0.95->1.0) with fastEasing', source: 'Web Micro-Transition', transition: RouteTransition.fade_scale, ).push(context), @@ -150,7 +157,9 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Adaptive Transitions --- - const _SectionTitle(title: '📱 Platform-Adaptive Transition'), + const _SectionTitle( + title: '📱 Platform-Adaptive Transition', + ), _ExampleButton( text: 'Browser.defaultAdaptiveTransition (Platform Auto-Detect)', onTap: () { @@ -161,7 +170,8 @@ class _HomeScreenState extends State { ), ); AppTrace.toPushArgs( - message: 'Auto-detected adaptive transition: ${adaptive.name}', + message: + 'Auto-detected adaptive transition: ${adaptive.name}', source: 'Browser.defaultAdaptiveTransition', transition: adaptive, ).push(context); @@ -170,14 +180,20 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Presentations & Modals --- - const _SectionTitle(title: '🪟 Presentation (TraceRoute) Styles'), + const _SectionTitle( + title: '🪟 Presentation (TraceRoute) Styles', + ), _ExampleButton( text: 'Present as Popup (Scale Transition)', - onTap: () => AppTrace.asPopup(transition: RouteTransition.scale).push(context), + onTap: () => + AppTrace.asPopup(transition: RouteTransition.scale) + .push(context), ), _ExampleButton( text: 'Present as Popup (Fade Scale Transition)', - onTap: () => AppTrace.asPopup(transition: RouteTransition.fade_scale).push(context), + onTap: () => AppTrace.asPopup( + transition: RouteTransition.fade_scale, + ).push(context), ), _ExampleButton( text: 'Present as Swipeable Bottom Sheet', @@ -186,7 +202,9 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Legacy Presets --- - const _SectionTitle(title: '📦 Legacy Slide & Fade Presets'), + const _SectionTitle( + title: '📦 Legacy Slide & Fade Presets', + ), _ExampleButton( text: 'Slide Right', onTap: () => AppTrace.toPushArgs( @@ -238,7 +256,9 @@ class _HomeScreenState extends State { const SizedBox(height: 20), // --- Argument Examples --- - const _SectionTitle(title: '🔗 Arguments & DeepLink Examples'), + const _SectionTitle( + title: '🔗 Arguments & DeepLink Examples', + ), _ExampleButton( text: '1. Push with Typed Arguments', onTap: () => AppTrace.toPushArgs( @@ -289,26 +309,23 @@ class _HomeScreenState extends State { const _SectionTitle(title: '📣 Overlay & Banners'), _ExampleButton( text: 'Enqueue Banner (Auto-dismiss 5s)', - onTap: () => Browser.enqueueBanner( - context, - (removableCallback) { - return const ColoredBox( - color: Color(0xFFF59E0B), - child: Padding( - padding: EdgeInsets.all(12), - child: Text( - '🔔 Real-time Notification Banner (Auto-closes in 5s)', - style: TextStyle( - color: Color(0xFF1E293B), - fontWeight: FontWeight.bold, + onTap: () => + Browser.enqueueBanner(context, (removableCallback) { + return const ColoredBox( + color: Color(0xFFF59E0B), + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + '🔔 Real-time Notification Banner (Auto-closes in 5s)', + style: TextStyle( + color: Color(0xFF1E293B), + fontWeight: FontWeight.bold, + ), + textDirection: TextDirection.ltr, ), - textDirection: TextDirection.ltr, ), - ), - ); - }, - topPadding: 0, - ), + ); + }, topPadding: 0), ), _ExampleButton( text: 'Enqueue Interactive Banner (Duration 0 + Close Button)', diff --git a/example/lib/main.dart b/example/lib/main.dart index 92d0e6c..ec70361 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -26,7 +26,10 @@ class MyApp extends StatelessWidget { onGenerateRoute: generate, onGenerateInitialRoutes: (routePath) => [ generate( - RouteSettings(name: routePath, arguments: const {}), + RouteSettings( + name: routePath, + arguments: const {}, + ), ), ], ); diff --git a/example/lib/pop_args_screen.dart b/example/lib/pop_args_screen.dart index b163879..2e9b41c 100644 --- a/example/lib/pop_args_screen.dart +++ b/example/lib/pop_args_screen.dart @@ -23,9 +23,7 @@ class PopArgsScreen extends StatelessWidget { GestureDetector( onTap: () { // Example 1: Pass arguments directly in pop() - context.pop( - args: PopResultArgs(result: 'Result from pop()'), - ); + context.pop(args: PopResultArgs(result: 'Result from pop()')); }, child: Container( color: const Color(0xFFFFFFFF), diff --git a/example/lib/pop_result_args.dart b/example/lib/pop_result_args.dart index a730183..32fedc4 100644 --- a/example/lib/pop_result_args.dart +++ b/example/lib/pop_result_args.dart @@ -1,9 +1,7 @@ import 'package:browser_router/browser.dart'; final class PopResultArgs extends RouteParams { - new({ - required this.result, - }); + new({required this.result}); final String result; diff --git a/example/lib/push_args.dart b/example/lib/push_args.dart index 2099e3b..4ee8bfd 100644 --- a/example/lib/push_args.dart +++ b/example/lib/push_args.dart @@ -1,10 +1,7 @@ import 'package:browser_router/browser.dart'; final class PushArgs extends RouteParams { - const new({ - required this.message, - required this.source, - }); + const new({required this.message, required this.source}); final String message; final String source; diff --git a/example/lib/push_args_screen.dart b/example/lib/push_args_screen.dart index 0b4df4b..430d8bc 100644 --- a/example/lib/push_args_screen.dart +++ b/example/lib/push_args_screen.dart @@ -41,8 +41,10 @@ class PushArgsScreen extends StatelessWidget { child: Text( 'Message: "${args.message}"\n(From: ${args.source})', textAlign: TextAlign.center, - style: - const TextStyle(color: Color(0xFFFFFFFF), fontSize: 18), + style: const TextStyle( + color: Color(0xFFFFFFFF), + fontSize: 18, + ), textDirection: TextDirection.ltr, ), ), diff --git a/example/lib/routes.dart b/example/lib/routes.dart index 6ce0deb..bebb071 100644 --- a/example/lib/routes.dart +++ b/example/lib/routes.dart @@ -23,10 +23,7 @@ final routes = [ return true; }, ), - BrowserRoute( - path: AppPath.popArgs.path, - page: const PopArgsScreen(), - ), + BrowserRoute(path: AppPath.popArgs.path, page: const PopArgsScreen()), BrowserRoute( path: AppPath.toSetPopArgs.path, page: const ToSetPopArgsScreen(), @@ -39,8 +36,5 @@ final routes = [ path: AppPath.intermediate.path, page: const IntermediateScreen(), ), - BrowserRoute( - path: AppPath.deep.path, - page: const DeepScreen(), - ), + BrowserRoute(path: AppPath.deep.path, page: const DeepScreen()), ]; diff --git a/lib/browser.dart b/lib/browser.dart index b0bebac..bc55e90 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -142,9 +142,12 @@ class Browser extends StatelessWidget { return RouteTransition.fade_scale; } return switch (defaultTargetPlatform) { - TargetPlatform.iOS || TargetPlatform.macOS => RouteTransition.slide_cupertino, - TargetPlatform.android || TargetPlatform.fuchsia => RouteTransition.shared_axis_x, - TargetPlatform.windows || TargetPlatform.linux => RouteTransition.fade_scale, + TargetPlatform.iOS || + TargetPlatform.macOS => RouteTransition.slide_cupertino, + TargetPlatform.android || + TargetPlatform.fuchsia => RouteTransition.shared_axis_x, + TargetPlatform.windows || + TargetPlatform.linux => RouteTransition.fade_scale, }; } @@ -167,7 +170,8 @@ class Browser extends StatelessWidget { final deepLinkParameter = uri?.queryParameters; final traceParameter = settings.arguments as Map?; - final traceRoute = adaptiveTrace?.call(name) ?? + final traceRoute = + adaptiveTrace?.call(name) ?? traceParameter?.getArgument() ?? const PageTraceRoute(); @@ -183,38 +187,37 @@ class Browser extends StatelessWidget { final appRoute = _obtainMainRoute(name, arguments); debugPrint('Browser.generate: appRoute: ${appRoute.path}'); - final browserRouteWithCustomTransition = - _browserRouteTransition(traceRoute, appRoute); + final browserRouteWithCustomTransition = _browserRouteTransition( + traceRoute, + appRoute, + ); debugPrint( 'Browser.generate: browserRouteWithCustomTransition: ${browserRouteWithCustomTransition.path}', ); - final newSettings = RouteSettings( - name: name, - arguments: arguments, - ); + final newSettings = RouteSettings(name: name, arguments: arguments); final route = switch (traceRoute) { PageTraceRoute() => BrowserPageRoute( - traceRoute: traceRoute, - appRoute: browserRouteWithCustomTransition, - settings: newSettings, - ), + traceRoute: traceRoute, + appRoute: browserRouteWithCustomTransition, + settings: newSettings, + ), SwipeTraceRoute() => BrowserSwipePopupRoute( - traceRoute: traceRoute, - appRoute: browserRouteWithCustomTransition, - settings: newSettings, - ), + traceRoute: traceRoute, + appRoute: browserRouteWithCustomTransition, + settings: newSettings, + ), PopupTraceRoute() => BrowserPopupRoute( - traceRoute: traceRoute, - appRoute: browserRouteWithCustomTransition, - settings: newSettings, - ), + traceRoute: traceRoute, + appRoute: browserRouteWithCustomTransition, + settings: newSettings, + ), OverlayTraceRoute() => BrowserPageRoute( - appRoute: browserRouteWithCustomTransition, - traceRoute: const PageTraceRoute(), - settings: newSettings, - ), + appRoute: browserRouteWithCustomTransition, + traceRoute: const PageTraceRoute(), + settings: newSettings, + ), }; debugPrint('Browser.generate: Returning route: $route'); return route; @@ -226,15 +229,13 @@ class Browser extends StatelessWidget { /// * When validate is incorrect default route will be return. /// /// In case of path is not found then will be use the default route. - BrowserRoute _obtainMainRoute( - String? name, - Map arguments, - ) { + BrowserRoute _obtainMainRoute(String? name, Map arguments) { debugPrint( 'Browser._obtainMainRoute: Called with name: $name, arguments: $arguments', ); - final appRoute = - routes.firstWhereOrNull((appRoute) => appRoute.path == name); + final appRoute = routes.firstWhereOrNull( + (appRoute) => appRoute.path == name, + ); debugPrint('Browser._obtainMainRoute: Found appRoute: ${appRoute?.path}'); final isValid = appRoute?.validateArguments?.call( @@ -298,36 +299,35 @@ class Browser extends StatelessWidget { bool enableDrag = true, bool useSafeArea = false, List? boxShadow, - }) => - Navigator.of(context).push( - BrowserSwipePopupRoute( - settings: routeSettings, - traceRoute: SwipeTraceRoute( - useSafeArea: useSafeArea, - enableDrag: enableDrag, - barrierColor: modalBarrierColor, - barrierDismissible: isDismissible, - screenMaximumPercentage: heightFactor, - animationDirection: animationDirection, + }) => Navigator.of(context).push( + BrowserSwipePopupRoute( + settings: routeSettings, + traceRoute: SwipeTraceRoute( + useSafeArea: useSafeArea, + enableDrag: enableDrag, + barrierColor: modalBarrierColor, + barrierDismissible: isDismissible, + screenMaximumPercentage: heightFactor, + animationDirection: animationDirection, + ), + appRoute: BrowserRoute( + path: '', + page: Container( + padding: padding, + decoration: BoxDecoration( + color: backgroundColor, + boxShadow: boxShadow, ), - appRoute: BrowserRoute( - path: '', - page: Container( - padding: padding, - decoration: BoxDecoration( - color: backgroundColor, - boxShadow: boxShadow, - ), - child: Builder( - builder: (context) { - return builder.call(context); - }, - ), - ), - routeTransition: RouteTransition.none, + child: Builder( + builder: (context) { + return builder.call(context); + }, ), ), - ); + routeTransition: RouteTransition.none, + ), + ), + ); /// Provides a widget that can listen to page appearance and disappearance events. /// @@ -349,10 +349,7 @@ class Browser extends StatelessWidget { static SingleChildStatelessWidget watch({ Key? key, void Function(BuildContext context)? onDisappear, - void Function( - BuildContext context, - DeepLinkParam? deepLinkParam, - )? onAppear, + void Function(BuildContext context, DeepLinkParam? deepLinkParam)? onAppear, Widget? child, }) { return PageObserverProvider( @@ -372,13 +369,12 @@ class Browser extends StatelessWidget { ContentBuilder content, { double? topPadding, Duration? duration = const Duration(seconds: 5), - }) => - OverlayManager.enqueueBanner( - context, - content: content, - topPadding: topPadding, - duration: duration, - ); + }) => OverlayManager.enqueueBanner( + context, + content: content, + topPadding: topPadding, + duration: duration, + ); /// Dismisses the currently visible overlay modal. static void dismissOverlay(BuildContext context) => @@ -403,15 +399,12 @@ class Browser extends StatelessWidget { useSafeArea: useSafeArea, alignment: builderAlignment, duration: null, - transition: transitionParams ?? + transition: + transitionParams ?? const OverlayTraceRoute( routeTransition: RouteTransition.fade, - transitionDuration: Duration( - milliseconds: 300, - ), - reverseTransitionDuration: Duration( - milliseconds: 300, - ), + transitionDuration: Duration(milliseconds: 300), + reverseTransitionDuration: Duration(milliseconds: 300), ), overlayState: Overlay.of(context), backgroundColor: backgroundColor, @@ -428,11 +421,7 @@ class Browser extends StatelessWidget { child: OverlayManager( child: Builder( builder: (context) { - return builder( - context, - routeObserver, - generate, - ); + return builder(context, routeObserver, generate); }, ), ), diff --git a/lib/browser_config.dart b/lib/browser_config.dart index fc5f015..ba0c2c4 100644 --- a/lib/browser_config.dart +++ b/lib/browser_config.dart @@ -14,22 +14,17 @@ class BrowserConfig extends InheritedWidget { final List routes; final Future Function(Uri uri)? openUrl; - static BrowserConfig of( - BuildContext context, { - bool build = false, - }) { + static BrowserConfig of(BuildContext context, {bool build = false}) { final conf = build ? context.dependOnInheritedWidgetOfExactType() : context.findAncestorWidgetOfExactType(); if (conf == null) { - throw FlutterError( - ''' + throw FlutterError(''' BrowserConfig.of() called with a context that does not contain a config. The widget used was: ${context.widget.runtimeType}. - ''', - ); + '''); } return conf; diff --git a/lib/browser_route.dart b/lib/browser_route.dart index 6e3b6e7..d9cb530 100755 --- a/lib/browser_route.dart +++ b/lib/browser_route.dart @@ -54,12 +54,10 @@ class BrowserRoute extends Equatable { /// validateArguments: (check, get) => check(), /// ``` final bool Function(Check checkArgument, GetArguments getArgument)? - validateArguments; + validateArguments; @override - List get props => [ - path, - ]; + List get props => [path]; BrowserRoute copyWith({ String? path, @@ -68,7 +66,7 @@ class BrowserRoute extends Equatable { BuildTransition? customTransition, void Function(BuildContext context)? builderTrigger, bool Function(Check checkArgument, GetArguments getArgument)? - validateArguments, + validateArguments, }) { return BrowserRoute( page: page ?? this.page, diff --git a/lib/deferred_browser_route.dart b/lib/deferred_browser_route.dart index 8f82811..a8acb56 100644 --- a/lib/deferred_browser_route.dart +++ b/lib/deferred_browser_route.dart @@ -13,20 +13,20 @@ class DeferredBrowserRoute extends BrowserRoute { super.builderTrigger, super.validateArguments, super.routeTransition, - }) : _initializeServiceLocator = initializeServiceLocator, - _loadPageLibrary = loadPageLibrary, - _page = pageBuilder, - super( - page: _DeferredPageLoader( - loadDeferredContent: _loadDeferred( - loadPageLibrary: loadPageLibrary, - initializeServiceLocator: initializeServiceLocator, - ), - onLoading: onLoading, - onError: onError, - page: pageBuilder, - ), - ); + }) : _initializeServiceLocator = initializeServiceLocator, + _loadPageLibrary = loadPageLibrary, + _page = pageBuilder, + super( + page: _DeferredPageLoader( + loadDeferredContent: _loadDeferred( + loadPageLibrary: loadPageLibrary, + initializeServiceLocator: initializeServiceLocator, + ), + onLoading: onLoading, + onError: onError, + page: pageBuilder, + ), + ); final Future Function() _loadPageLibrary; final Future Function()? _initializeServiceLocator; diff --git a/lib/gestures/swipe/swipe.dart b/lib/gestures/swipe/swipe.dart index 4fb566a..1e99a2a 100644 --- a/lib/gestures/swipe/swipe.dart +++ b/lib/gestures/swipe/swipe.dart @@ -8,9 +8,9 @@ part 'swipe_transition_builder.dart'; extension GenericAxis on AxisDirection { Axis generic() => switch (this) { - AxisDirection.up || AxisDirection.down => Axis.vertical, - AxisDirection.right || AxisDirection.left => Axis.horizontal, - }; + AxisDirection.up || AxisDirection.down => Axis.vertical, + AxisDirection.right || AxisDirection.left => Axis.horizontal, + }; } class Swipe extends StatelessWidget { @@ -26,32 +26,32 @@ class Swipe extends StatelessWidget { this.direction = AxisDirection.up, super.key, }) : _child = hasEnableGestures - ? NotificationListener( - onNotification: onNotification, - child: GestureDetector( - excludeFromSemantics: true, - onVerticalDragStart: direction.generic() == Axis.vertical - ? gestures?.handleDragStart - : null, - onVerticalDragEnd: direction.generic() == Axis.vertical - ? gestures?.handleDragEnd - : null, - onVerticalDragUpdate: direction.generic() == Axis.vertical - ? gestures?.handleDragUpdate - : null, - onHorizontalDragStart: direction.generic() == Axis.horizontal - ? gestures?.handleDragStart - : null, - onHorizontalDragUpdate: direction.generic() == Axis.horizontal - ? gestures?.handleDragUpdate - : null, - onHorizontalDragEnd: direction.generic() == Axis.horizontal - ? gestures?.handleDragEnd - : null, - child: child, - ), - ) - : child; + ? NotificationListener( + onNotification: onNotification, + child: GestureDetector( + excludeFromSemantics: true, + onVerticalDragStart: direction.generic() == Axis.vertical + ? gestures?.handleDragStart + : null, + onVerticalDragEnd: direction.generic() == Axis.vertical + ? gestures?.handleDragEnd + : null, + onVerticalDragUpdate: direction.generic() == Axis.vertical + ? gestures?.handleDragUpdate + : null, + onHorizontalDragStart: direction.generic() == Axis.horizontal + ? gestures?.handleDragStart + : null, + onHorizontalDragUpdate: direction.generic() == Axis.horizontal + ? gestures?.handleDragUpdate + : null, + onHorizontalDragEnd: direction.generic() == Axis.horizontal + ? gestures?.handleDragEnd + : null, + child: child, + ), + ) + : child; final Animation animation; final Widget _child; diff --git a/lib/gestures/swipe/swipe_animation.dart b/lib/gestures/swipe/swipe_animation.dart index 2d50524..f1330ff 100644 --- a/lib/gestures/swipe/swipe_animation.dart +++ b/lib/gestures/swipe/swipe_animation.dart @@ -11,10 +11,10 @@ class SwipeAnimation extends StatefulWidget { required this.animationDirection, super.key, }) : assert( - !enableDrag || animationController != null, - "'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. " - "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", - ); + !enableDrag || animationController != null, + "'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. " + "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", + ); final Animation animation; final WidgetBuilder builder; final double screenMaximumPercentage; @@ -34,35 +34,35 @@ class _SwipeAnimationState extends State { SwipeGestures _generateSwipeGestures({required AxisDirection direction}) => switch (direction) { AxisDirection.up => SwipeDownRightGestures( - animationController: widget.animationController!, - obtainSize: () => context.size?.height ?? 0, - canDragDone: () => - widget.animationController?.status == AnimationStatus.reverse, - onClosing: widget.onClosing, - ), + animationController: widget.animationController!, + obtainSize: () => context.size?.height ?? 0, + canDragDone: () => + widget.animationController?.status == AnimationStatus.reverse, + onClosing: widget.onClosing, + ), AxisDirection.left => SwipeDownRightGestures( - animationController: widget.animationController!, - obtainSize: () => context.size?.width ?? 0, - canDragDone: () => - widget.animationController?.status == AnimationStatus.reverse, - onClosing: widget.onClosing, - closePercentage: 0.8, - ), + animationController: widget.animationController!, + obtainSize: () => context.size?.width ?? 0, + canDragDone: () => + widget.animationController?.status == AnimationStatus.reverse, + onClosing: widget.onClosing, + closePercentage: 0.8, + ), AxisDirection.down => SwipeUpLeftGestures( - animationController: widget.animationController!, - obtainSize: () => context.size?.height ?? 0, - canDragDone: () => - widget.animationController?.status == AnimationStatus.reverse, - onClosing: widget.onClosing, - ), + animationController: widget.animationController!, + obtainSize: () => context.size?.height ?? 0, + canDragDone: () => + widget.animationController?.status == AnimationStatus.reverse, + onClosing: widget.onClosing, + ), AxisDirection.right => SwipeUpLeftGestures( - animationController: widget.animationController!, - obtainSize: () => context.size?.width ?? 0, - canDragDone: () => - widget.animationController?.status == AnimationStatus.reverse, - onClosing: widget.onClosing, - closePercentage: 0.8, - ), + animationController: widget.animationController!, + obtainSize: () => context.size?.width ?? 0, + canDragDone: () => + widget.animationController?.status == AnimationStatus.reverse, + onClosing: widget.onClosing, + closePercentage: 0.8, + ), }; @override @@ -78,9 +78,7 @@ class _SwipeAnimationState extends State { direction: widget.animationDirection, animateChild: true, gestures: widget.enableDrag && widget.animationController != null - ? _generateSwipeGestures( - direction: widget.animationDirection, - ) + ? _generateSwipeGestures(direction: widget.animationDirection) : null, onNotification: (notification) { if (notification.extent == notification.minExtent) { @@ -89,10 +87,7 @@ class _SwipeAnimationState extends State { return false; }, disableAnimations: MediaQuery.disableAnimationsOf(context), - child: KeyedSubtree( - key: _childKey, - child: widget.builder(context), - ), + child: KeyedSubtree(key: _childKey, child: widget.builder(context)), ); } } diff --git a/lib/gestures/swipe/swipe_gestures.dart b/lib/gestures/swipe/swipe_gestures.dart index df56099..291288c 100644 --- a/lib/gestures/swipe/swipe_gestures.dart +++ b/lib/gestures/swipe/swipe_gestures.dart @@ -83,8 +83,8 @@ final class SwipeDownRightGestures extends SwipeGestures { } if (delta > 0 || animationController.value > 0) { - animationController.value = - (animationController.value - delta / size).clamp(0.0, 1.0); + animationController.value = (animationController.value - delta / size) + .clamp(0.0, 1.0); } } @@ -142,8 +142,8 @@ final class SwipeUpLeftGestures extends SwipeGestures { } if (delta > 0 || animationController.value > 0) { - animationController.value = - (animationController.value + delta / size).clamp(0.0, 1.0); + animationController.value = (animationController.value + delta / size) + .clamp(0.0, 1.0); } } } diff --git a/lib/gestures/swipe/swipe_transition_builder.dart b/lib/gestures/swipe/swipe_transition_builder.dart index f3845ed..28393c8 100644 --- a/lib/gestures/swipe/swipe_transition_builder.dart +++ b/lib/gestures/swipe/swipe_transition_builder.dart @@ -1,11 +1,7 @@ part of 'swipe.dart'; final class SwipeChildLayoutDelegate extends SingleChildLayoutDelegate { - const new( - this.progress, - this.screenPercentage, - this.direction, - ); + const new(this.progress, this.screenPercentage, this.direction); final double progress; final double screenPercentage; @@ -23,21 +19,20 @@ final class SwipeChildLayoutDelegate extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) => switch (direction) { - AxisDirection.up => - Offset(0, size.height - childSize.height * progress), - AxisDirection.down => Offset( - 0, - size.height - (progress == 0 ? size.height : childSize.height), - ), - AxisDirection.right => Offset( - (childSize.width * progress) - size.width, - size.height - childSize.height, - ), - AxisDirection.left => Offset( - size.width - childSize.width * progress, - size.height - childSize.height, - ), - }; + AxisDirection.up => Offset(0, size.height - childSize.height * progress), + AxisDirection.down => Offset( + 0, + size.height - (progress == 0 ? size.height : childSize.height), + ), + AxisDirection.right => Offset( + (childSize.width * progress) - size.width, + size.height - childSize.height, + ), + AxisDirection.left => Offset( + size.width - childSize.width * progress, + size.height - childSize.height, + ), + }; @override bool shouldRelayout(SwipeChildLayoutDelegate oldDelegate) { diff --git a/lib/modal_route/browser_page_route.dart b/lib/modal_route/browser_page_route.dart index bdc6026..791e30d 100755 --- a/lib/modal_route/browser_page_route.dart +++ b/lib/modal_route/browser_page_route.dart @@ -9,29 +9,26 @@ export 'params/trace_route.dart' show PageTraceRoute; class BrowserPageRoute extends PageRoute with BrowserModalBarrierMixin { - new({ - required this.appRoute, - required this.traceRoute, - super.settings, - }) : transitionDuration = - (traceRoute.routeTransition ?? appRoute.routeTransition) == - RouteTransition.none - ? Duration.zero - : traceRoute.transitionDuration, - reverseTransitionDuration = - (traceRoute.routeTransition ?? appRoute.routeTransition) == - RouteTransition.none - ? Duration.zero - : traceRoute.reverseTransitionDuration, - barrierLabel = traceRoute.barrierLabel, - maintainState = traceRoute.maintainState, - opaque = traceRoute.opaque, - barrierDismissible = traceRoute.barrierDismissible, - barrierColor = traceRoute.barrierColor, - super( - allowSnapshotting: traceRoute.allowSnapshotting, - fullscreenDialog: traceRoute.fullScreenDialog, - ) { + new({required this.appRoute, required this.traceRoute, super.settings}) + : transitionDuration = + (traceRoute.routeTransition ?? appRoute.routeTransition) == + RouteTransition.none + ? Duration.zero + : traceRoute.transitionDuration, + reverseTransitionDuration = + (traceRoute.routeTransition ?? appRoute.routeTransition) == + RouteTransition.none + ? Duration.zero + : traceRoute.reverseTransitionDuration, + barrierLabel = traceRoute.barrierLabel, + maintainState = traceRoute.maintainState, + opaque = traceRoute.opaque, + barrierDismissible = traceRoute.barrierDismissible, + barrierColor = traceRoute.barrierColor, + super( + allowSnapshotting: traceRoute.allowSnapshotting, + fullscreenDialog: traceRoute.fullScreenDialog, + ) { debugPrint('BrowserPageRoute: Constructor called for ${settings.name}'); } @@ -99,11 +96,13 @@ class BrowserPageRoute extends PageRoute } final isolatedChild = RepaintBoundary(child: child); - final buildTransition = traceRoute.customTransition ?? + final buildTransition = + traceRoute.customTransition ?? appRoute.customTransition ?? (traceRoute.routeTransition ?? appRoute.routeTransition).build; - final isSlide = appRoute.routeTransition == RouteTransition.slide_right || + final isSlide = + appRoute.routeTransition == RouteTransition.slide_right || appRoute.routeTransition == RouteTransition.slide_cupertino; if (defaultTargetPlatform == TargetPlatform.iOS && diff --git a/lib/modal_route/browser_popup_route.dart b/lib/modal_route/browser_popup_route.dart index e5f843f..eed348e 100644 --- a/lib/modal_route/browser_popup_route.dart +++ b/lib/modal_route/browser_popup_route.dart @@ -8,30 +8,27 @@ export 'params/trace_route.dart' show PopupTraceRoute; class BrowserPopupRoute extends PopupRoute with BrowserModalBarrierMixin { /// A modal popup route. - new({ - required this.traceRoute, - required this.appRoute, - super.settings, - }) : transitionDuration = - (traceRoute.routeTransition ?? appRoute.routeTransition) == - RouteTransition.none - ? Duration.zero - : traceRoute.transitionDuration, - reverseTransitionDuration = - (traceRoute.routeTransition ?? appRoute.routeTransition) == - RouteTransition.none - ? Duration.zero - : traceRoute.reverseTransitionDuration, - barrierLabel = traceRoute.barrierLabel, - maintainState = traceRoute.maintainState, - opaque = traceRoute.opaque, - barrierDismissible = traceRoute.barrierDismissible, - barrierColor = traceRoute.barrierColor, - allowSnapshotting = traceRoute.allowSnapshotting, - super( - filter: traceRoute.filter, - traversalEdgeBehavior: traceRoute.traversalEdgeBehavior, - ); + new({required this.traceRoute, required this.appRoute, super.settings}) + : transitionDuration = + (traceRoute.routeTransition ?? appRoute.routeTransition) == + RouteTransition.none + ? Duration.zero + : traceRoute.transitionDuration, + reverseTransitionDuration = + (traceRoute.routeTransition ?? appRoute.routeTransition) == + RouteTransition.none + ? Duration.zero + : traceRoute.reverseTransitionDuration, + barrierLabel = traceRoute.barrierLabel, + maintainState = traceRoute.maintainState, + opaque = traceRoute.opaque, + barrierDismissible = traceRoute.barrierDismissible, + barrierColor = traceRoute.barrierColor, + allowSnapshotting = traceRoute.allowSnapshotting, + super( + filter: traceRoute.filter, + traversalEdgeBehavior: traceRoute.traversalEdgeBehavior, + ); final BrowserRoute appRoute; final P traceRoute; @@ -93,7 +90,8 @@ class BrowserPopupRoute extends PopupRoute } final isolatedChild = RepaintBoundary(child: child); - final buildTransition = traceRoute.customTransition ?? + final buildTransition = + traceRoute.customTransition ?? appRoute.customTransition ?? (traceRoute.routeTransition ?? appRoute.routeTransition).build; diff --git a/lib/modal_route/browser_swipe_popup_route.dart b/lib/modal_route/browser_swipe_popup_route.dart index 0c7a1e0..4ada19e 100644 --- a/lib/modal_route/browser_swipe_popup_route.dart +++ b/lib/modal_route/browser_swipe_popup_route.dart @@ -10,11 +10,7 @@ export 'params/trace_route.dart' show SwipeTraceRoute; class BrowserSwipePopupRoute extends BrowserPopupRoute with BrowserModalBarrierMixin { /// A modal bottom sheet route. - new({ - required super.traceRoute, - required super.appRoute, - super.settings, - }); + new({required super.traceRoute, required super.appRoute, super.settings}); @override Widget buildPage( diff --git a/lib/modal_route/params/trace_route.dart b/lib/modal_route/params/trace_route.dart index 512e0b9..1dd111b 100644 --- a/lib/modal_route/params/trace_route.dart +++ b/lib/modal_route/params/trace_route.dart @@ -55,24 +55,23 @@ sealed class TraceRoute { String semanticsLabel = '', bool fullscreenDialog = false, bool popGestureEnabled = true, - }) => - PageTraceRoute( - routeTransition: routeTransition, - customTransition: customTransition, - opaque: opaque, - maintainState: maintainState, - allowSnapshotting: allowSnapshotting, - filter: filter, - traversalEdgeBehavior: traversalEdgeBehavior, - barrierColor: barrierColor, - reverseTransitionDuration: reverseTransitionDuration, - transitionDuration: transitionDuration, - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - semanticsLabel: semanticsLabel, - fullScreenDialog: fullscreenDialog, - popGestureEnabled: popGestureEnabled, - ); + }) => PageTraceRoute( + routeTransition: routeTransition, + customTransition: customTransition, + opaque: opaque, + maintainState: maintainState, + allowSnapshotting: allowSnapshotting, + filter: filter, + traversalEdgeBehavior: traversalEdgeBehavior, + barrierColor: barrierColor, + reverseTransitionDuration: reverseTransitionDuration, + transitionDuration: transitionDuration, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + semanticsLabel: semanticsLabel, + fullScreenDialog: fullscreenDialog, + popGestureEnabled: popGestureEnabled, + ); /// Creates a [PopupTraceRoute] for presenting a route as a modal dialog /// or popup. @@ -90,22 +89,21 @@ sealed class TraceRoute { bool barrierDismissible = true, String barrierLabel = '', String semanticsLabel = '', - }) => - PopupTraceRoute( - routeTransition: routeTransition, - customTransition: customTransition, - opaque: opaque, - maintainState: maintainState, - allowSnapshotting: allowSnapshotting, - filter: filter, - traversalEdgeBehavior: traversalEdgeBehavior, - barrierColor: barrierColor, - reverseTransitionDuration: reverseTransitionDuration, - transitionDuration: transitionDuration, - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - semanticsLabel: semanticsLabel, - ); + }) => PopupTraceRoute( + routeTransition: routeTransition, + customTransition: customTransition, + opaque: opaque, + maintainState: maintainState, + allowSnapshotting: allowSnapshotting, + filter: filter, + traversalEdgeBehavior: traversalEdgeBehavior, + barrierColor: barrierColor, + reverseTransitionDuration: reverseTransitionDuration, + transitionDuration: transitionDuration, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + semanticsLabel: semanticsLabel, + ); /// Creates a [SwipeTraceRoute] for presenting a route as a swipeable /// bottom sheet. @@ -128,27 +126,26 @@ sealed class TraceRoute { bool enableDrag = true, double screenMaximumPercentage = 1, Offset? anchorPoint, - }) => - SwipeTraceRoute( - routeTransition: routeTransition, - customTransition: customTransition, - opaque: opaque, - maintainState: maintainState, - allowSnapshotting: allowSnapshotting, - filter: filter, - traversalEdgeBehavior: traversalEdgeBehavior, - barrierColor: barrierColor, - reverseTransitionDuration: reverseTransitionDuration, - transitionDuration: transitionDuration, - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - semanticsLabel: semanticsLabel, - useSafeArea: useSafeArea, - animationDirection: animationDirection, - enableDrag: enableDrag, - screenMaximumPercentage: screenMaximumPercentage, - anchorPoint: anchorPoint, - ); + }) => SwipeTraceRoute( + routeTransition: routeTransition, + customTransition: customTransition, + opaque: opaque, + maintainState: maintainState, + allowSnapshotting: allowSnapshotting, + filter: filter, + traversalEdgeBehavior: traversalEdgeBehavior, + barrierColor: barrierColor, + reverseTransitionDuration: reverseTransitionDuration, + transitionDuration: transitionDuration, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + semanticsLabel: semanticsLabel, + useSafeArea: useSafeArea, + animationDirection: animationDirection, + enableDrag: enableDrag, + screenMaximumPercentage: screenMaximumPercentage, + anchorPoint: anchorPoint, + ); /// The transition animation to use when the route is pushed or popped. /// If null, the default transition from `BrowserRoute` is used. diff --git a/lib/modal_route/shared_modal_barrier.dart b/lib/modal_route/shared_modal_barrier.dart index 566327b..c6d1c98 100644 --- a/lib/modal_route/shared_modal_barrier.dart +++ b/lib/modal_route/shared_modal_barrier.dart @@ -14,11 +14,7 @@ mixin BrowserModalBarrierMixin on ModalRoute { ColorTween( begin: barrierColor?.withValues(alpha: 0), end: barrierColor, - ).chain( - CurveTween( - curve: barrierCurve, - ), - ), + ).chain(CurveTween(curve: barrierCurve)), ); barrier = Builder( builder: (context) { diff --git a/lib/modal_route/transitions/build_transitions/custom_build_transition.dart b/lib/modal_route/transitions/build_transitions/custom_build_transition.dart index 7fb2c15..5088a14 100644 --- a/lib/modal_route/transitions/build_transitions/custom_build_transition.dart +++ b/lib/modal_route/transitions/build_transitions/custom_build_transition.dart @@ -18,7 +18,8 @@ final class CustomBuildTransition implements BuildTransition { required Animation animation, required Animation secondaryAnimation, required Widget child, - }) builder; + }) + builder; @override Widget call({ diff --git a/lib/modal_route/transitions/build_transitions/fade_build_transition.dart b/lib/modal_route/transitions/build_transitions/fade_build_transition.dart index 2ce0a24..6b9a5d4 100644 --- a/lib/modal_route/transitions/build_transitions/fade_build_transition.dart +++ b/lib/modal_route/transitions/build_transitions/fade_build_transition.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; + import 'build_transition.dart'; import 'widgets/fade.dart'; diff --git a/lib/modal_route/transitions/build_transitions/no_build_transition.dart b/lib/modal_route/transitions/build_transitions/no_build_transition.dart index 6561775..83d429f 100644 --- a/lib/modal_route/transitions/build_transitions/no_build_transition.dart +++ b/lib/modal_route/transitions/build_transitions/no_build_transition.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; + import 'build_transition.dart'; final class NoBuildTransition implements BuildTransition { diff --git a/lib/modal_route/transitions/build_transitions/slide_build_transition.dart b/lib/modal_route/transitions/build_transitions/slide_build_transition.dart index b076efb..65a7044 100644 --- a/lib/modal_route/transitions/build_transitions/slide_build_transition.dart +++ b/lib/modal_route/transitions/build_transitions/slide_build_transition.dart @@ -1,11 +1,10 @@ import 'package:flutter/widgets.dart'; + import 'build_transition.dart'; import 'widgets/slide.dart'; final class SlideBuildTransition implements BuildTransition { - const new({ - required this.position, - }); + const new({required this.position}); final Positions position; diff --git a/lib/modal_route/transitions/build_transitions/widgets/fade.dart b/lib/modal_route/transitions/build_transitions/widgets/fade.dart index f83277a..b129f16 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/fade.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/fade.dart @@ -1,11 +1,7 @@ import 'package:flutter/widgets.dart'; class Fade extends StatelessWidget { - const new({ - required this.animation, - required this.child, - super.key, - }); + const new({required this.animation, required this.child, super.key}); final Animation animation; final Widget child; @@ -17,9 +13,6 @@ class Fade extends StatelessWidget { curve: Curves.easeIn, reverseCurve: Curves.easeIn, ); - return FadeTransition( - opacity: curvedAnimation, - child: child, - ); + return FadeTransition(opacity: curvedAnimation, child: child); } } diff --git a/lib/modal_route/transitions/build_transitions/widgets/fade_scale.dart b/lib/modal_route/transitions/build_transitions/widgets/fade_scale.dart index e8884d3..1440a36 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/fade_scale.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/fade_scale.dart @@ -10,32 +10,25 @@ class FadeScale extends StatelessWidget { required this.secondaryAnimation, required this.child, super.key, - }) : _primaryOpacity = CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - _primaryScale = Tween(begin: 0.95, end: 1).animate( - CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ), - _secondaryOpacity = Tween(begin: 1, end: 0).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ), - _secondaryScale = Tween(begin: 1, end: 0.95).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ); + }) : _primaryOpacity = CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + _primaryScale = Tween(begin: 0.95, end: 1).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ), + _secondaryScale = Tween(begin: 1, end: 0.95).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ); final Animation animation; final Animation secondaryAnimation; @@ -43,22 +36,15 @@ class FadeScale extends StatelessWidget { final Animation _primaryOpacity; final Animation _primaryScale; - final Animation _secondaryOpacity; final Animation _secondaryScale; @override Widget build(BuildContext context) { - return FadeTransition( - opacity: _secondaryOpacity, - child: ScaleTransition( - scale: _secondaryScale, - child: FadeTransition( - opacity: _primaryOpacity, - child: ScaleTransition( - scale: _primaryScale, - child: child, - ), - ), + return ScaleTransition( + scale: _secondaryScale, + child: FadeTransition( + opacity: _primaryOpacity, + child: ScaleTransition(scale: _primaryScale, child: child), ), ); } diff --git a/lib/modal_route/transitions/build_transitions/widgets/fade_through.dart b/lib/modal_route/transitions/build_transitions/widgets/fade_through.dart index ec3146f..f476871 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/fade_through.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/fade_through.dart @@ -11,28 +11,40 @@ class FadeThrough extends StatelessWidget { required this.child, this.fillColor, super.key, - }) : _primaryOpacity = CurvedAnimation( - parent: animation, - curve: const Interval(0.35, 1, curve: Curves.fastEaseInToSlowEaseOut), - ), - _primaryScale = Tween(begin: 0.92, end: 1).animate( - CurvedAnimation( - parent: animation, - curve: const Interval(0.35, 1, curve: Curves.fastEaseInToSlowEaseOut), - ), - ), - _secondaryOpacity = Tween(begin: 1, end: 0).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: const Interval(0, 0.35, curve: Curves.fastEaseInToSlowEaseOut), - ), - ), - _secondaryScale = Tween(begin: 1, end: 0.96).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: const Interval(0, 0.35, curve: Curves.fastEaseInToSlowEaseOut), - ), - ); + }) : _primaryOpacity = CurvedAnimation( + parent: animation, + curve: const Interval(0.35, 1, curve: Curves.fastEaseInToSlowEaseOut), + ), + _primaryScale = Tween(begin: 0.92, end: 1).animate( + CurvedAnimation( + parent: animation, + curve: const Interval( + 0.35, + 1, + curve: Curves.fastEaseInToSlowEaseOut, + ), + ), + ), + _secondaryOpacity = Tween(begin: 1, end: 0).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: const Interval( + 0, + 0.35, + curve: Curves.fastEaseInToSlowEaseOut, + ), + ), + ), + _secondaryScale = Tween(begin: 1, end: 0.96).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: const Interval( + 0, + 0.35, + curve: Curves.fastEaseInToSlowEaseOut, + ), + ), + ); final Animation animation; final Animation secondaryAnimation; @@ -46,18 +58,21 @@ class FadeThrough extends StatelessWidget { @override Widget build(BuildContext context) { - return FadeTransition( + Widget content = FadeTransition( opacity: _secondaryOpacity, child: ScaleTransition( scale: _secondaryScale, child: FadeTransition( opacity: _primaryOpacity, - child: ScaleTransition( - scale: _primaryScale, - child: child, - ), + child: ScaleTransition(scale: _primaryScale, child: child), ), ), ); + + if (fillColor != null) { + content = ColoredBox(color: fillColor!, child: content); + } + + return content; } } diff --git a/lib/modal_route/transitions/build_transitions/widgets/scale.dart b/lib/modal_route/transitions/build_transitions/widgets/scale.dart index 16acb11..342bfb6 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/scale.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/scale.dart @@ -8,25 +8,18 @@ class Scale extends StatelessWidget { required this.child, this.initialScale = 0.85, super.key, - }) : _scale = Tween(begin: initialScale, end: 1).animate( - CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ), - _opacity = CurvedAnimation( - parent: animation, - curve: Curves.easeOut, - reverseCurve: Curves.easeIn, - ), - _secondaryOpacity = Tween(begin: 1, end: 0).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.easeOut, - reverseCurve: Curves.easeIn, - ), - ); + }) : _scale = Tween(begin: initialScale, end: 1).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ), + _opacity = CurvedAnimation( + parent: animation, + curve: Curves.easeOut, + reverseCurve: Curves.easeIn, + ); final Animation animation; final Animation secondaryAnimation; @@ -35,19 +28,12 @@ class Scale extends StatelessWidget { final Animation _scale; final Animation _opacity; - final Animation _secondaryOpacity; @override Widget build(BuildContext context) { return FadeTransition( - opacity: _secondaryOpacity, - child: FadeTransition( - opacity: _opacity, - child: ScaleTransition( - scale: _scale, - child: child, - ), - ), + opacity: _opacity, + child: ScaleTransition(scale: _scale, child: child), ); } } diff --git a/lib/modal_route/transitions/build_transitions/widgets/shared_axis.dart b/lib/modal_route/transitions/build_transitions/widgets/shared_axis.dart index ca8212c..9be63b7 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/shared_axis.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/shared_axis.dart @@ -1,10 +1,6 @@ import 'package:flutter/widgets.dart'; -enum SharedAxisDirection { - horizontal, - vertical, - scaled, -} +enum SharedAxisDirection { horizontal, vertical, scaled } /// Material 3 Shared Axis Transition. /// @@ -16,16 +12,10 @@ class SharedAxis extends StatelessWidget { required this.child, this.direction = SharedAxisDirection.horizontal, super.key, - }) : _primaryOpacity = CurvedAnimation( - parent: animation, - curve: const Interval(0.1, 1, curve: Curves.fastEaseInToSlowEaseOut), - ), - _secondaryOpacity = Tween(begin: 1, end: 0).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: const Interval(0, 0.4, curve: Curves.fastEaseInToSlowEaseOut), - ), - ); + }) : _primaryOpacity = CurvedAnimation( + parent: animation, + curve: const Interval(0.1, 1, curve: Curves.fastEaseInToSlowEaseOut), + ); final Animation animation; final Animation secondaryAnimation; @@ -33,7 +23,6 @@ class SharedAxis extends StatelessWidget { final SharedAxisDirection direction; final Animation _primaryOpacity; - final Animation _secondaryOpacity; @override Widget build(BuildContext context) { @@ -48,77 +37,67 @@ class SharedAxis extends StatelessWidget { final textDirection = Directionality.of(context); final isRtl = textDirection == TextDirection.rtl; - final primaryOffset = Tween( - begin: Offset(isRtl ? -0.3 : 0.3, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ); + final primaryOffset = + Tween( + begin: Offset(isRtl ? -0.3 : 0.3, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ); - final secondaryOffset = Tween( - begin: Offset.zero, - end: Offset(isRtl ? 0.3 : -0.3, 0), - ).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ); + final secondaryOffset = + Tween( + begin: Offset.zero, + end: Offset(isRtl ? 0.3 : -0.3, 0), + ).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ); return SlideTransition( position: secondaryOffset, - child: FadeTransition( - opacity: _secondaryOpacity, - child: SlideTransition( - position: primaryOffset, - child: FadeTransition( - opacity: _primaryOpacity, - child: child, - ), - ), + textDirection: textDirection, + transformHitTests: false, + child: SlideTransition( + position: primaryOffset, + textDirection: textDirection, + child: FadeTransition(opacity: _primaryOpacity, child: child), ), ); } Widget _buildVertical(BuildContext context) { - final primaryOffset = Tween( - begin: const Offset(0, 0.3), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ); + final primaryOffset = + Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ); - final secondaryOffset = Tween( - begin: Offset.zero, - end: const Offset(0, -0.3), - ).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ); + final secondaryOffset = + Tween(begin: Offset.zero, end: const Offset(0, -0.3)).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ); return SlideTransition( position: secondaryOffset, - child: FadeTransition( - opacity: _secondaryOpacity, - child: SlideTransition( - position: primaryOffset, - child: FadeTransition( - opacity: _primaryOpacity, - child: child, - ), - ), + transformHitTests: false, + child: SlideTransition( + position: primaryOffset, + child: FadeTransition(opacity: _primaryOpacity, child: child), ), ); } @@ -142,15 +121,9 @@ class SharedAxis extends StatelessWidget { return ScaleTransition( scale: secondaryScale, - child: FadeTransition( - opacity: _secondaryOpacity, - child: ScaleTransition( - scale: primaryScale, - child: FadeTransition( - opacity: _primaryOpacity, - child: child, - ), - ), + child: ScaleTransition( + scale: primaryScale, + child: FadeTransition(opacity: _primaryOpacity, child: child), ), ); } diff --git a/lib/modal_route/transitions/build_transitions/widgets/slide.dart b/lib/modal_route/transitions/build_transitions/widgets/slide.dart index ab2545a..a6075fa 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/slide.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/slide.dart @@ -6,31 +6,15 @@ import 'package:flutter/widgets.dart'; enum Positions { // Offset from offscreen below to fully on screen. - left( - primaryOffset: Offset(-1, 0), - secondaryOffset: Offset(1 / 3, 0), - ), - right( - primaryOffset: Offset(1, 0), - secondaryOffset: Offset(-1 / 3, 0), - ), + left(primaryOffset: Offset(-1, 0), secondaryOffset: Offset(1 / 3, 0)), + right(primaryOffset: Offset(1, 0), secondaryOffset: Offset(-1 / 3, 0)), //Esto esta seteado a 0, 0 para que las navegaciones no se rompan en las pantallas que se navega sobre ellas. //Estaría bien setear en la navegación el secondary del navegador anterior // Offset(0, -1 / 3), - up( - primaryOffset: Offset(0, 1), - secondaryOffset: Offset.zero, - ), - down( - primaryOffset: Offset(0, -1), - secondaryOffset: Offset(0, 1 / 3), - ), - ; + up(primaryOffset: Offset(0, 1), secondaryOffset: Offset.zero), + down(primaryOffset: Offset(0, -1), secondaryOffset: Offset(0, 1 / 3)); - new({ - required this.primaryOffset, - required this.secondaryOffset, - }); + new({required this.primaryOffset, required this.secondaryOffset}); final Offset primaryOffset; final Offset secondaryOffset; } @@ -42,34 +26,24 @@ class Slide extends StatelessWidget { required Positions drive, required this.child, super.key, - }) : _primaryPositionAnimation = CurvedAnimation( - // The curves below have been rigorously derived from plots of native - // iOS animation frames. Specifically, a video was taken of a page - // transition animation and the distance in each frame that the page - // moved was measured. A best fit bezier curve was the fitted to the - // point set, which is linearToEaseIn. Conversely, easeInToLinear is the - // reflection over the origin of linearToEaseIn. - parent: primaryRouteAnimation, - // curve: Curves.linearToEaseOut, - // reverseCurve: Curves.easeInToLinear, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ).drive( - Tween( - begin: drive.primaryOffset, - end: Offset.zero, - ), - ), - _secondaryPositionAnimation = CurvedAnimation( - parent: secondaryRouteAnimation, - curve: Curves.linearToEaseOut, - reverseCurve: Curves.easeInToLinear, - ).drive( - Tween( - begin: Offset.zero, - end: drive.secondaryOffset, - ), - ); + }) : _primaryPositionAnimation = CurvedAnimation( + // The curves below have been rigorously derived from plots of native + // iOS animation frames. Specifically, a video was taken of a page + // transition animation and the distance in each frame that the page + // moved was measured. A best fit bezier curve was the fitted to the + // point set, which is linearToEaseIn. Conversely, easeInToLinear is the + // reflection over the origin of linearToEaseIn. + parent: primaryRouteAnimation, + // curve: Curves.linearToEaseOut, + // reverseCurve: Curves.easeInToLinear, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ).drive(Tween(begin: drive.primaryOffset, end: Offset.zero)), + _secondaryPositionAnimation = CurvedAnimation( + parent: secondaryRouteAnimation, + curve: Curves.linearToEaseOut, + reverseCurve: Curves.easeInToLinear, + ).drive(Tween(begin: Offset.zero, end: drive.secondaryOffset)); final Animation primaryRouteAnimation; // When this page is coming in to cover another page. @@ -95,9 +69,7 @@ class Slide extends StatelessWidget { ), end: BoxDecoration( color: const Color.fromARGB(0, 255, 255, 255), - border: Border.all( - style: BorderStyle.none, - ), + border: Border.all(style: BorderStyle.none), borderRadius: BorderRadius.zero, // No shadow. ), diff --git a/lib/modal_route/transitions/build_transitions/widgets/slide_cupertino.dart b/lib/modal_route/transitions/build_transitions/widgets/slide_cupertino.dart index ac98aef..d14d912 100644 --- a/lib/modal_route/transitions/build_transitions/widgets/slide_cupertino.dart +++ b/lib/modal_route/transitions/build_transitions/widgets/slide_cupertino.dart @@ -11,26 +11,25 @@ class SlideCupertino extends StatelessWidget { required this.child, this.hasShadow = true, super.key, - }) : _primaryPosition = Tween( - begin: const Offset(1, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, - ), - ), - _secondaryPosition = Tween( - begin: Offset.zero, - end: const Offset(-1 / 3, 0), - ).animate( - CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.linearToEaseOut, - reverseCurve: Curves.easeInToLinear, - ), - ); + }) : _primaryPosition = + Tween(begin: const Offset(1, 0), end: Offset.zero).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, + ), + ), + _secondaryPosition = + Tween( + begin: Offset.zero, + end: const Offset(-1 / 3, 0), + ).animate( + CurvedAnimation( + parent: secondaryAnimation, + curve: Curves.linearToEaseOut, + reverseCurve: Curves.easeInToLinear, + ), + ); final Animation animation; final Animation secondaryAnimation; diff --git a/lib/modal_route/transitions/route_transition.dart b/lib/modal_route/transitions/route_transition.dart index 089c941..32cb0cf 100644 --- a/lib/modal_route/transitions/route_transition.dart +++ b/lib/modal_route/transitions/route_transition.dart @@ -21,15 +21,9 @@ export 'build_transitions/slide_build_transition.dart'; export 'build_transitions/slide_cupertino_build_transition.dart'; enum RouteTransition { - fade( - build: FadeBuildTransition(), - ), - fade_through( - build: FadeThroughBuildTransition(), - ), - fade_scale( - build: FadeScaleBuildTransition(), - ), + fade(build: FadeBuildTransition()), + fade_through(build: FadeThroughBuildTransition()), + fade_scale(build: FadeScaleBuildTransition()), shared_axis_x( build: SharedAxisBuildTransition(direction: SharedAxisDirection.horizontal), ), @@ -39,39 +33,14 @@ enum RouteTransition { shared_axis_z( build: SharedAxisBuildTransition(direction: SharedAxisDirection.scaled), ), - scale( - build: ScaleBuildTransition(), - ), - slide_cupertino( - build: SlideCupertinoBuildTransition(), - ), - slide_right( - build: SlideBuildTransition( - position: Positions.right, - ), - ), - slide_up( - build: SlideBuildTransition( - position: Positions.up, - ), - ), - slide_left( - build: SlideBuildTransition( - position: Positions.left, - ), - ), - slide_down( - build: SlideBuildTransition( - position: Positions.down, - ), - ), - only_hero( - build: NoBuildTransition(), - ), - none( - build: NoBuildTransition(), - ), - ; + scale(build: ScaleBuildTransition()), + slide_cupertino(build: SlideCupertinoBuildTransition()), + slide_right(build: SlideBuildTransition(position: Positions.right)), + slide_up(build: SlideBuildTransition(position: Positions.up)), + slide_left(build: SlideBuildTransition(position: Positions.left)), + slide_down(build: SlideBuildTransition(position: Positions.down)), + only_hero(build: NoBuildTransition()), + none(build: NoBuildTransition()); new({required this.build}); diff --git a/lib/navigator_extension.dart b/lib/navigator_extension.dart index a5a0fca..f4178c9 100644 --- a/lib/navigator_extension.dart +++ b/lib/navigator_extension.dart @@ -43,8 +43,7 @@ extension _Args on Map { extension NavigatorX on BuildContext { Map _createArguments(List? args) { final arguments = { - for (final argument in (args ?? [])) - argument.runtimeType: ?argument, + for (final argument in (args ?? [])) argument.runtimeType: ?argument, }; return arguments; } @@ -79,9 +78,7 @@ extension NavigatorX on BuildContext { /// See also: /// /// * [getArgument], for reading an argument without consuming it. - T? getArgumentAndClean({ - RouteSettings? settings, - }) { + T? getArgumentAndClean({RouteSettings? settings}) { final arguments = settings?.arguments ?? ModalRoute.of(this)?.settings.arguments; @@ -90,9 +87,7 @@ extension NavigatorX on BuildContext { return arguments.getAndClean(); } - void cleanArguments({ - RouteSettings? settings, - }) { + void cleanArguments({RouteSettings? settings}) { final arguments = settings?.arguments ?? ModalRoute.of(this)?.settings.arguments; @@ -101,9 +96,9 @@ extension NavigatorX on BuildContext { } NavigatorState get navigate => Navigator.of( - this, - // rootNavigator: true - ); + this, + // rootNavigator: true + ); /// Navigates to a new route by its `path`. /// @@ -118,15 +113,10 @@ extension NavigatorX on BuildContext { RouteSettings? settings, NavigatorState? navigator, }) { - final pushParams = getArgumentAndClean<_PushParam>( - settings: settings, - )?.pushParams; - - final argsMap = _createArguments([ - ...args, - pushParams, - ?traceRoute, - ]); + final pushParams = getArgumentAndClean<_PushParam>(settings: settings) + ?.pushParams; + + final argsMap = _createArguments([...args, pushParams, ?traceRoute]); return (navigator ?? navigate).pushNamed(path, arguments: argsMap); } @@ -137,16 +127,13 @@ extension NavigatorX on BuildContext { RouteSettings? settings, NavigatorState? navigator, }) { - final pushParams = getArgumentAndClean<_PushParam>( - settings: settings, - )?.pushParams; - final argsMap = _createArguments([ - ...args, - pushParams, - ?traceRoute, - ]); - return (navigator ?? navigate) - .pushReplacementNamed(path, arguments: argsMap); + final pushParams = getArgumentAndClean<_PushParam>(settings: settings) + ?.pushParams; + final argsMap = _createArguments([...args, pushParams, ?traceRoute]); + return (navigator ?? navigate).pushReplacementNamed( + path, + arguments: argsMap, + ); } /// Pops the current route off the navigator. @@ -159,9 +146,8 @@ extension NavigatorX on BuildContext { T? args, RouteSettings? settings, }) async { - final popParams = getArgumentAndClean<_PopParam>( - settings: settings, - )?.popParams; + final popParams = getArgumentAndClean<_PopParam>(settings: settings) + ?.popParams; final navigator = navigate; if (navigator.canPop()) { @@ -171,15 +157,13 @@ extension NavigatorX on BuildContext { return false; } if (route.settings.arguments is Map) { - _createArguments([args, popParams]).forEach( - (key, value) { - ((route.settings.arguments as Map?) ?? {}).update( - key, - (value) => value, - ifAbsent: () => value, - ); - }, - ); + _createArguments([args, popParams]).forEach((key, value) { + ((route.settings.arguments as Map?) ?? {}).update( + key, + (value) => value, + ifAbsent: () => value, + ); + }); } return true; }); @@ -248,17 +232,13 @@ extension NavigatorX on BuildContext { List args = const [], RouteSettings? settings, }) async { - final popParams = getArgumentAndClean<_PopParam>( - settings: settings, - )?.popParams; + final popParams = getArgumentAndClean<_PopParam>(settings: settings) + ?.popParams; if (!navigate.canPop()) { await pushNamed( BrowserConfig.of(this).defaultRoute.path, - args: [ - ...args, - if (popParams is T) popParams, - ], + args: [...args, if (popParams is T) popParams], ); return; } @@ -266,15 +246,13 @@ extension NavigatorX on BuildContext { return navigate.popUntil((route) { if (route.isFirst) { if (route.settings.arguments is Map) { - _createArguments([...args, popParams]).forEach( - (key, value) { - (route.settings.arguments! as Map).update( - key, - (value) => value, - ifAbsent: () => value, - ); - }, - ); + _createArguments([...args, popParams]).forEach((key, value) { + (route.settings.arguments! as Map).update( + key, + (value) => value, + ifAbsent: () => value, + ); + }); } return true; } else { @@ -290,11 +268,7 @@ extension NavigatorX on BuildContext { final argument = _PopParam(routeParams); - arguments.update( - _PopParam, - (value) => argument, - ifAbsent: () => argument, - ); + arguments.update(_PopParam, (value) => argument, ifAbsent: () => argument); } void setPushArgument(RouteParams routeParams) { @@ -304,11 +278,7 @@ extension NavigatorX on BuildContext { final argument = _PushParam(routeParams); - arguments.update( - _PushParam, - (value) => argument, - ifAbsent: () => argument, - ); + arguments.update(_PushParam, (value) => argument, ifAbsent: () => argument); } bool canNavigate(String path) { diff --git a/lib/overlay/modal.dart b/lib/overlay/modal.dart index 6732955..5c60a47 100644 --- a/lib/overlay/modal.dart +++ b/lib/overlay/modal.dart @@ -21,10 +21,7 @@ class Modal extends OverlayModal { OverlayEntry _createModal(Widget child) { final overlay = ColoredBox( color: backgroundColor, - child: Align( - alignment: alignment, - child: child, - ), + child: Align(alignment: alignment, child: child), ); return OverlayEntry( diff --git a/lib/overlay/overlay_manager.dart b/lib/overlay/overlay_manager.dart index 2848caa..ba0a579 100755 --- a/lib/overlay/overlay_manager.dart +++ b/lib/overlay/overlay_manager.dart @@ -14,10 +14,7 @@ part 'modal.dart'; part 'overlay_modal.dart'; class OverlayManager extends StatefulWidget { - const new({ - required this.child, - super.key, - }); + const new({required this.child, super.key}); final Widget child; @@ -75,9 +72,7 @@ class OverlayManagerState extends State { ///Show a [OverlayModal] in foreground /// ///Only one modal can be display at the same time if you want to insert multiple OverlayModal show [enqueue] function. - Future showModal( - OverlayModal overlayModal, - ) async { + Future showModal(OverlayModal overlayModal) async { if (mounted) { await dismissModal(); modal = overlayModal; @@ -106,9 +101,7 @@ class OverlayManagerState extends State { } } - Future _work( - OverlayModal overlayModal, - ) async { + Future _work(OverlayModal overlayModal) async { try { if (mounted) { poolCurrentOverlayModal = overlayModal; diff --git a/lib/overlay/overlay_modal.dart b/lib/overlay/overlay_modal.dart index 2bac740..20d9035 100644 --- a/lib/overlay/overlay_modal.dart +++ b/lib/overlay/overlay_modal.dart @@ -11,12 +11,12 @@ abstract class OverlayModal { required this._content, required this.transition, required OverlayState overlayState, - }) : _overlayState = overlayState, - _animationController = AnimationController( - vsync: overlayState, - duration: transition.transitionDuration, - reverseDuration: transition.reverseTransitionDuration, - ); + }) : _overlayState = overlayState, + _animationController = AnimationController( + vsync: overlayState, + duration: transition.transitionDuration, + reverseDuration: transition.reverseTransitionDuration, + ); //constructor fields final OverlayState _overlayState; @@ -54,10 +54,7 @@ abstract class OverlayModal { if (_isDisposed) return; if (duration is Duration && duration != Duration.zero) { - timer = Timer( - duration!, - remove, - ); + timer = Timer(duration!, remove); } await completer?.future; diff --git a/lib/route_arguments.dart b/lib/route_arguments.dart index 22a4a72..7d7b097 100644 --- a/lib/route_arguments.dart +++ b/lib/route_arguments.dart @@ -27,9 +27,7 @@ abstract base class RouteParams { } final class DataParam extends RouteParams { - const new( - this.data, - ); + const new(this.data); final T? data; } @@ -40,25 +38,19 @@ final class DataParam extends RouteParams { /// an instance of [DeepLinkParam] containing `{'foo': 'bar'}` is automatically /// added to the route's arguments. final class DeepLinkParam extends RouteParams { - const new( - this.params, - ); + const new(this.params); final Map params; } final class _PopParam extends RouteParams { - const new( - this.popParams, - ); + const new(this.popParams); final RouteParams popParams; } final class _PushParam extends RouteParams { - const new( - this.pushParams, - ); + const new(this.pushParams); final RouteParams pushParams; } diff --git a/lib/route_observer.dart b/lib/route_observer.dart index 7b8f801..3e43356 100644 --- a/lib/route_observer.dart +++ b/lib/route_observer.dart @@ -9,14 +9,10 @@ class PageObserverProvider extends SingleChildStatelessWidget { super.child, }); - final void Function( - BuildContext context, - DeepLinkParam? deepLinkParam, - )? onAppear; + final void Function(BuildContext context, DeepLinkParam? deepLinkParam)? + onAppear; - final void Function( - BuildContext context, - )? onDisappear; + final void Function(BuildContext context)? onDisappear; final RouteObserver> routeObserver; @@ -50,44 +46,35 @@ class RouteObserverProvider extends SingleChildStatefulWidget { /// Called when the top route has been popped off, and the current route /// shows up. /// !appears - final void Function( - BuildContext context, - )? didPopNext; + final void Function(BuildContext context)? didPopNext; /// Called when the top route has been popped off, and the current route /// shows up with arguments. /// !appears - final void Function( - BuildContext context, - DeepLinkParam? deepLinkParam, - )? didPopNextWithArguments; + final void Function(BuildContext context, DeepLinkParam? deepLinkParam)? + didPopNextWithArguments; /// Called when the current route has been pushed. /// !appears - final void Function( - BuildContext context, - DeepLinkParam? deepLinkParam, - )? didPush; + final void Function(BuildContext context, DeepLinkParam? deepLinkParam)? + didPush; /// Called when the current route has been popped off. /// !disappear - final void Function( - BuildContext context, - )? didPop; + final void Function(BuildContext context)? didPop; /// Called when a new route has been pushed, and the current route is no /// longer visible. /// !disappear - final void Function( - BuildContext context, - )? didPushNext; + final void Function(BuildContext context)? didPushNext; @override State createState() => _RouteObserverProviderState(); } class _RouteObserverProviderState - extends SingleChildState with RouteAware { + extends SingleChildState + with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/sheet/modal_base.dart b/lib/sheet/modal_base.dart index ca4e77f..4ab4cd5 100644 --- a/lib/sheet/modal_base.dart +++ b/lib/sheet/modal_base.dart @@ -8,9 +8,7 @@ abstract class ModalBase { final T params; - ModalBaseHeaderParameter contextParameters({ - required BuildContext context, - }); + ModalBaseHeaderParameter contextParameters({required BuildContext context}); Widget body({ required BuildContext context, @@ -29,14 +27,11 @@ abstract class ModalBase { } final class ModalBaseSafeArea { - const new({ - required this.external, - required this.internal, - }); + const new({required this.external, required this.internal}); const new cleanTopSafeArea() - : external = const SafeAreaManager.fromLTRB(true, true, true, false), - internal = const SafeAreaManager.all(); + : external = const SafeAreaManager.fromLTRB(true, true, true, false), + internal = const SafeAreaManager.all(); final SafeAreaManager external; final SafeAreaManager internal; @@ -58,13 +53,7 @@ final class SafeAreaManager { this.bottom, ); - const new all() - : this( - top: true, - bottom: true, - left: true, - right: true, - ); + const new all() : this(top: true, bottom: true, left: true, right: true); final bool top; final bool bottom; @@ -72,10 +61,10 @@ final class SafeAreaManager { final bool right; SafeArea safeArea({required Widget child}) => SafeArea( - bottom: bottom, - left: left, - top: top, - right: right, - child: child, - ); + bottom: bottom, + left: left, + top: top, + right: right, + child: child, + ); } diff --git a/lib/sheet/modal_base_header.dart b/lib/sheet/modal_base_header.dart index bbd1b3f..29a396c 100644 --- a/lib/sheet/modal_base_header.dart +++ b/lib/sheet/modal_base_header.dart @@ -31,15 +31,15 @@ abstract class ModalBaseHeader extends SliverPersistentHeaderDelegate { double get maxExtent => (!kIsWeb && snap) ? 57 : (parameters.title != null) - ? 57 - : 27; + ? 57 + : 27; @override double get minExtent => (!kIsWeb && snap) ? 57 : (parameters.title != null) - ? 57 - : 27; + ? 57 + : 27; @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => @@ -51,9 +51,5 @@ abstract class ModalBaseHeader extends SliverPersistentHeaderDelegate { final BorderRadiusGeometry? border; @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ); + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent); } diff --git a/lib/sheet/modal_base_params.dart b/lib/sheet/modal_base_params.dart index 871dbf6..54ced4d 100644 --- a/lib/sheet/modal_base_params.dart +++ b/lib/sheet/modal_base_params.dart @@ -28,16 +28,16 @@ base class ModalDraggableScrollableSheetParams extends ModalBaseParams { Duration? snapAnimationDuration, bool shouldCloseOnMinExtent = true, }) : this( - initialHeightChildSize: initialHeightChildSize, - minHeightChildSize: minHeightChildSize, - maxHeightChildSize: maxHeightChildSize, - withChildSize: withChildSize, - expand: expand, - snap: snap, - snapSizes: snapSizes, - snapAnimationDuration: snapAnimationDuration, - shouldCloseOnMinExtent: shouldCloseOnMinExtent, - ); + initialHeightChildSize: initialHeightChildSize, + minHeightChildSize: minHeightChildSize, + maxHeightChildSize: maxHeightChildSize, + withChildSize: withChildSize, + expand: expand, + snap: snap, + snapSizes: snapSizes, + snapAnimationDuration: snapAnimationDuration, + shouldCloseOnMinExtent: shouldCloseOnMinExtent, + ); const new medium({ double initialHeightChildSize = 0.4, @@ -50,16 +50,16 @@ base class ModalDraggableScrollableSheetParams extends ModalBaseParams { Duration? snapAnimationDuration, bool shouldCloseOnMinExtent = true, }) : this( - initialHeightChildSize: initialHeightChildSize, - minHeightChildSize: minHeightChildSize, - maxHeightChildSize: maxHeightChildSize, - withChildSize: withChildSize, - expand: expand, - snap: snap, - snapSizes: snapSizes, - snapAnimationDuration: snapAnimationDuration, - shouldCloseOnMinExtent: shouldCloseOnMinExtent, - ); + initialHeightChildSize: initialHeightChildSize, + minHeightChildSize: minHeightChildSize, + maxHeightChildSize: maxHeightChildSize, + withChildSize: withChildSize, + expand: expand, + snap: snap, + snapSizes: snapSizes, + snapAnimationDuration: snapAnimationDuration, + shouldCloseOnMinExtent: shouldCloseOnMinExtent, + ); const new large({ double initialHeightChildSize = 0.45, @@ -72,16 +72,16 @@ base class ModalDraggableScrollableSheetParams extends ModalBaseParams { Duration? snapAnimationDuration, bool shouldCloseOnMinExtent = true, }) : this( - initialHeightChildSize: initialHeightChildSize, - minHeightChildSize: minHeightChildSize, - maxHeightChildSize: maxHeightChildSize, - withChildSize: withChildSize, - expand: expand, - snap: snap, - snapSizes: snapSizes, - snapAnimationDuration: snapAnimationDuration, - shouldCloseOnMinExtent: shouldCloseOnMinExtent, - ); + initialHeightChildSize: initialHeightChildSize, + minHeightChildSize: minHeightChildSize, + maxHeightChildSize: maxHeightChildSize, + withChildSize: withChildSize, + expand: expand, + snap: snap, + snapSizes: snapSizes, + snapAnimationDuration: snapAnimationDuration, + shouldCloseOnMinExtent: shouldCloseOnMinExtent, + ); /// The initial fractional value of the parent container's height to use when /// displaying the widget. diff --git a/lib/sheet/sheet.dart b/lib/sheet/sheet.dart index 9aa034e..562234e 100644 --- a/lib/sheet/sheet.dart +++ b/lib/sheet/sheet.dart @@ -25,15 +25,12 @@ class Sheet extends StatelessWidget { } static BrowserRoute get route => const BrowserRoute( - path: sheetPath, - page: Sheet(), - routeTransition: RouteTransition.none, - ); - - static Future show( - BuildContext context, - Widget child, - ) async { + path: sheetPath, + page: Sheet(), + routeTransition: RouteTransition.none, + ); + + static Future show(BuildContext context, Widget child) async { await Trace( path: sheetPath, traceRoute: TraceRoute.popup(), @@ -43,9 +40,7 @@ class Sheet extends StatelessWidget { } final class SheetRouteParams extends RouteParams { - new({ - required this.child, - }); + new({required this.child}); final Widget child; } diff --git a/lib/sheet/sheet_base.dart b/lib/sheet/sheet_base.dart index 3ca9f3d..b4553f1 100644 --- a/lib/sheet/sheet_base.dart +++ b/lib/sheet/sheet_base.dart @@ -6,10 +6,7 @@ typedef ScrollableBuilder = Widget Function( ); abstract class SheetBase extends StatelessWidget { - const new({ - required this.modal, - super.key, - }); + const new({required this.modal, super.key}); final ModalBase modal; @@ -29,29 +26,20 @@ abstract class SheetBase extends StatelessWidget { PrimaryScrollController scrollController({ required BuildContext context, required Widget child, - }) => - PrimaryScrollController.none(child: child); + }) => PrimaryScrollController.none(child: child); - Widget sheetArea({ - required BuildContext context, - required Widget child, - }) { + Widget sheetArea({required BuildContext context, required Widget child}) { final keyboard = MediaQuery.viewInsetsOf(context).bottom; return Padding( - padding: EdgeInsets.only( - bottom: keyboard, - ), + padding: EdgeInsets.only(bottom: keyboard), child: child, ); } BorderRadiusGeometry? get borderRadius; - EdgeInsets get bodyPadding => const EdgeInsets.only( - left: 16, - right: 16, - ); + EdgeInsets get bodyPadding => const EdgeInsets.only(left: 16, right: 16); EdgeInsets get buttonBarPadding => const EdgeInsets.only(left: 16, right: 16, bottom: 16); diff --git a/lib/trace.dart b/lib/trace.dart index a5c371a..fdd452c 100644 --- a/lib/trace.dart +++ b/lib/trace.dart @@ -26,18 +26,10 @@ part of 'browser.dart'; /// AppTraces.toProfile('123').push(context); /// ``` class Trace { - const new({ - required this.path, - this.traceRoute, - this.args, - }); + const new({required this.path, this.traceRoute, this.args}); Future push(BuildContext context) { - return context.pushNamed( - path, - args: [args], - traceRoute: traceRoute, - ); + return context.pushNamed(path, args: [args], traceRoute: traceRoute); } Future pushAndReplacement(BuildContext context) { @@ -49,24 +41,15 @@ class Trace { } Future cleanAndPush(BuildContext context) { - return context.popToFirstAndPushReplacementNamed( - path, - args: [args], - ); + return context.popToFirstAndPushReplacementNamed(path, args: [args]); } Future popToFirstAndPush(BuildContext context) { - return context.popToFirstAndPushNamed( - path, - args: [args], - ); + return context.popToFirstAndPushNamed(path, args: [args]); } Future findMeOrPush(BuildContext context) { - return context.popToSelectOrFirstAndPushNamed( - path, - args: [args], - ); + return context.popToSelectOrFirstAndPushNamed(path, args: [args]); } final String path; diff --git a/pubspec.yaml b/pubspec.yaml index 4a48513..4a23bb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: browser_router description: An advanced navigation system for Flutter that enables typed routes, custom transitions, and robust overlay management. -version: 0.3.0 +version: 0.3.1 repository: https://github.com/coolosos/browser_router homepage: https://github.com/coolosos/browser_router issue_tracker: https://github.com/coolosos/browser_router/issues diff --git a/test/browser_routes_and_barriers_test.dart b/test/browser_routes_and_barriers_test.dart index 38411b7..a0e3e11 100644 --- a/test/browser_routes_and_barriers_test.dart +++ b/test/browser_routes_and_barriers_test.dart @@ -84,7 +84,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -107,9 +110,7 @@ void main() { } }); - testWidgets('BrowserPopupRoute properties and debugLabel', ( - tester, - ) async { + testWidgets('BrowserPopupRoute properties and debugLabel', (tester) async { const appRoute = BrowserRoute( path: '/popup', page: Text('Popup Content', textDirection: TextDirection.ltr), @@ -132,67 +133,71 @@ void main() { expect(popupRoute.debugLabel, contains('/popup')); }); - testWidgets('BrowserSwipePopupRoute buildPage with useSafeArea true and false', ( - tester, - ) async { - const routes = [ - BrowserRoute( - path: '/home', - page: Text('Home Screen', textDirection: TextDirection.ltr), - ), - ]; + testWidgets( + 'BrowserSwipePopupRoute buildPage with useSafeArea true and false', + (tester) async { + const routes = [ + BrowserRoute( + path: '/home', + page: Text('Home Screen', textDirection: TextDirection.ltr), + ), + ]; - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) => WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - onGenerateInitialRoutes: (path) => [ - generate( - RouteSettings(name: path, arguments: const {}), - ), - ], + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) => WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + onGenerateInitialRoutes: (path) => [ + generate( + RouteSettings( + name: path, + arguments: const {}, + ), + ), + ], + ), ), - ), - ); - await tester.pumpAndSettle(); + ); + await tester.pumpAndSettle(); - final context = tester.element(find.text('Home Screen')); + final context = tester.element(find.text('Home Screen')); - // 1. Swipe route with useSafeArea: true - final swipeRouteSafe = BrowserSwipePopupRoute( - traceRoute: const SwipeTraceRoute(useSafeArea: true), - appRoute: const BrowserRoute( - path: '/sheet_safe', - page: Text('Safe Sheet', textDirection: TextDirection.ltr), - ), - settings: const RouteSettings(name: '/sheet_safe'), - ); - Navigator.of(context).push(swipeRouteSafe); - await tester.pumpAndSettle(); - expect(find.text('Safe Sheet'), findsOneWidget); + // 1. Swipe route with useSafeArea: true + final swipeRouteSafe = BrowserSwipePopupRoute( + traceRoute: const SwipeTraceRoute(useSafeArea: true), + appRoute: const BrowserRoute( + path: '/sheet_safe', + page: Text('Safe Sheet', textDirection: TextDirection.ltr), + ), + settings: const RouteSettings(name: '/sheet_safe'), + ); + Navigator.of(context).push(swipeRouteSafe); + await tester.pumpAndSettle(); + expect(find.text('Safe Sheet'), findsOneWidget); - final safeContext = tester.element(find.text('Safe Sheet')); - await safeContext.pop(); - await tester.pumpAndSettle(); + final safeContext = tester.element(find.text('Safe Sheet')); + await safeContext.pop(); + await tester.pumpAndSettle(); - // 2. Swipe route with useSafeArea: false - final swipeRouteUnsafe = BrowserSwipePopupRoute( - traceRoute: const SwipeTraceRoute(useSafeArea: false), - appRoute: const BrowserRoute( - path: '/sheet_unsafe', - page: Text('Unsafe Sheet', textDirection: TextDirection.ltr), - ), - settings: const RouteSettings(name: '/sheet_unsafe'), - ); - final currentContext = tester.element(find.text('Home Screen')); - Navigator.of(currentContext).push(swipeRouteUnsafe); - await tester.pumpAndSettle(); - expect(find.text('Unsafe Sheet'), findsOneWidget); - }); + // 2. Swipe route with useSafeArea: false + final swipeRouteUnsafe = BrowserSwipePopupRoute( + traceRoute: const SwipeTraceRoute(useSafeArea: false), + appRoute: const BrowserRoute( + path: '/sheet_unsafe', + page: Text('Unsafe Sheet', textDirection: TextDirection.ltr), + ), + settings: const RouteSettings(name: '/sheet_unsafe'), + ); + final currentContext = tester.element(find.text('Home Screen')); + Navigator.of(currentContext).push(swipeRouteUnsafe); + await tester.pumpAndSettle(); + expect(find.text('Unsafe Sheet'), findsOneWidget); + }, + ); }); group('BrowserModalBarrierMixin & Modal/Banner Tests', () { @@ -206,7 +211,9 @@ void main() { ), BrowserRoute( path: '/popup', - page: Center(child: Text('Popup Content', textDirection: TextDirection.ltr)), + page: Center( + child: Text('Popup Content', textDirection: TextDirection.ltr), + ), ), ]; @@ -220,7 +227,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -252,9 +262,7 @@ void main() { expect(find.text('Home Screen'), findsOneWidget); }); - testWidgets('Modal alignment fix and properties', ( - tester, - ) async { + testWidgets('Modal alignment fix and properties', (tester) async { const routes = [ BrowserRoute( path: '/home', @@ -272,7 +280,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -289,7 +300,8 @@ void main() { isDismissible: true, useSafeArea: true, builderAlignment: Alignment.bottomCenter, - builder: (dismiss) => const Text('Aligned Modal', textDirection: TextDirection.ltr), + builder: (dismiss) => + const Text('Aligned Modal', textDirection: TextDirection.ltr), ); await tester.pumpAndSettle(); @@ -310,9 +322,7 @@ void main() { expect(find.text('Aligned Modal'), findsNothing); }); - testWidgets('Banner.fromContext and swipe dismiss up', ( - tester, - ) async { + testWidgets('Banner.fromContext and swipe dismiss up', (tester) async { const routes = [ BrowserRoute( path: '/home', @@ -330,7 +340,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -342,7 +355,8 @@ void main() { unawaited( Banner.fromContext( context: homeContext, - content: (dismiss) => const Text('Swipe Up Banner', textDirection: TextDirection.ltr), + content: (dismiss) => + const Text('Swipe Up Banner', textDirection: TextDirection.ltr), transition: const OverlayTraceRoute(), ).insert(), ); @@ -350,7 +364,11 @@ void main() { expect(find.text('Swipe Up Banner'), findsOneWidget); // Dismiss by swiping up - await tester.fling(find.text('Swipe Up Banner'), const Offset(0, -300), 1000); + await tester.fling( + find.text('Swipe Up Banner'), + const Offset(0, -300), + 1000, + ); await tester.pumpAndSettle(); expect(find.text('Swipe Up Banner'), findsNothing); @@ -381,7 +399,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), diff --git a/test/browser_widget_test.dart b/test/browser_widget_test.dart index 9fe7407..6c0c915 100644 --- a/test/browser_widget_test.dart +++ b/test/browser_widget_test.dart @@ -42,7 +42,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -88,7 +91,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -99,110 +105,121 @@ void main() { expect(capturedTransition, equals(RouteTransition.fade)); }); - testWidgets('adaptiveTrace determines presentation style based on route name', ( - tester, - ) async { - final routes = [ - const BrowserRoute( - path: '/home', - page: Text('Home Screen', textDirection: TextDirection.ltr), - ), - const BrowserRoute( - path: '/popup_modal', - page: Text('Modal Content', textDirection: TextDirection.ltr), - ), - ]; - - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - adaptiveTrace: (name) { - if (name == '/popup_modal') { - return const PopupTraceRoute( - routeTransition: RouteTransition.fade, - ); - } - return null; - }, - builder: (context, routeObserver, generate) => WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - onGenerateInitialRoutes: (path) => [ - generate( - RouteSettings(name: path, arguments: const {}), - ), - ], + testWidgets( + 'adaptiveTrace determines presentation style based on route name', + (tester) async { + final routes = [ + const BrowserRoute( + path: '/home', + page: Text('Home Screen', textDirection: TextDirection.ltr), ), - ), - ); - await tester.pumpAndSettle(); - - final homeContext = tester.element(find.text('Home Screen')); - unawaited(homeContext.pushNamed('/popup_modal')); - await tester.pumpAndSettle(); - - expect(find.text('Modal Content'), findsOneWidget); - }); - - testWidgets('validateArguments redirects to defaultRoute when validation fails', ( - tester, - ) async { - final routes = [ - const BrowserRoute( - path: '/home', - page: Text('Home Screen', textDirection: TextDirection.ltr), - ), - BrowserRoute( - path: '/protected', - page: const Text('Protected Screen', textDirection: TextDirection.ltr), - validateArguments: (check, get) => check(), - ), - ]; - - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) => WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - onGenerateInitialRoutes: (path) => [ - generate( - RouteSettings(name: path, arguments: const {}), - ), - ], + const BrowserRoute( + path: '/popup_modal', + page: Text('Modal Content', textDirection: TextDirection.ltr), ), - ), - ); - await tester.pumpAndSettle(); + ]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + adaptiveTrace: (name) { + if (name == '/popup_modal') { + return const PopupTraceRoute( + routeTransition: RouteTransition.fade, + ); + } + return null; + }, + builder: (context, routeObserver, generate) => WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + onGenerateInitialRoutes: (path) => [ + generate( + RouteSettings( + name: path, + arguments: const {}, + ), + ), + ], + ), + ), + ); + await tester.pumpAndSettle(); + + final homeContext = tester.element(find.text('Home Screen')); + unawaited(homeContext.pushNamed('/popup_modal')); + await tester.pumpAndSettle(); + + expect(find.text('Modal Content'), findsOneWidget); + }, + ); + + testWidgets( + 'validateArguments redirects to defaultRoute when validation fails', + (tester) async { + final routes = [ + const BrowserRoute( + path: '/home', + page: Text('Home Screen', textDirection: TextDirection.ltr), + ), + BrowserRoute( + path: '/protected', + page: const Text( + 'Protected Screen', + textDirection: TextDirection.ltr, + ), + validateArguments: (check, get) => check(), + ), + ]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) => WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + onGenerateInitialRoutes: (path) => [ + generate( + RouteSettings( + name: path, + arguments: const {}, + ), + ), + ], + ), + ), + ); + await tester.pumpAndSettle(); - final homeContext = tester.element(find.text('Home Screen')); + final homeContext = tester.element(find.text('Home Screen')); - // 1. Invalid argument -> redirected to defaultRoute (/home) - unawaited( - homeContext.pushNamed( - '/protected', - args: [const ValidatedArgs('INVALID_CODE')], - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Protected Screen'), findsNothing); - expect(find.text('Home Screen'), findsOneWidget); - - // 2. Valid argument -> loads protected screen - final homeContext2 = tester.element(find.text('Home Screen')); - unawaited( - homeContext2.pushNamed( - '/protected', - args: [const ValidatedArgs('VALID')], - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Protected Screen'), findsOneWidget); - }); + // 1. Invalid argument -> redirected to defaultRoute (/home) + unawaited( + homeContext.pushNamed( + '/protected', + args: [const ValidatedArgs('INVALID_CODE')], + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Protected Screen'), findsNothing); + expect(find.text('Home Screen'), findsOneWidget); + + // 2. Valid argument -> loads protected screen + final homeContext2 = tester.element(find.text('Home Screen')); + unawaited( + homeContext2.pushNamed( + '/protected', + args: [const ValidatedArgs('VALID')], + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Protected Screen'), findsOneWidget); + }, + ); testWidgets('DeepLinkParam extracts query parameters automatically', ( tester, @@ -266,7 +283,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -323,7 +343,10 @@ void main() { onGenerateRoute: generate, onGenerateInitialRoutes: (path) => [ generate( - RouteSettings(name: path, arguments: const {}), + RouteSettings( + name: path, + arguments: const {}, + ), ), ], ), @@ -361,80 +384,93 @@ void main() { ); }); - testWidgets('Browser overlay helpers showModalBottomSheet, showOverlay, dismissOverlay, enqueueBanner', ( - tester, - ) async { - final routes = [ - const BrowserRoute( - path: '/home', - page: Text('Home Screen', textDirection: TextDirection.ltr), - ), - ]; - - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) => WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - onGenerateInitialRoutes: (path) => [ - generate( - RouteSettings(name: path, arguments: const {}), - ), - ], + testWidgets( + 'Browser overlay helpers showModalBottomSheet, showOverlay, dismissOverlay, enqueueBanner', + (tester) async { + final routes = [ + const BrowserRoute( + path: '/home', + page: Text('Home Screen', textDirection: TextDirection.ltr), ), - ), - ); - await tester.pumpAndSettle(); - - final context = tester.element(find.text('Home Screen')); - - // 1. showOverlay & dismissOverlay - Browser.showOverlay( - context, - backgroundColor: const Color(0x80000000), - isDismissible: true, - builder: (dismiss) => const Text('Custom Overlay Text', textDirection: TextDirection.ltr), - ); - await tester.pumpAndSettle(); - expect(find.text('Custom Overlay Text'), findsOneWidget); - - Browser.dismissOverlay(context); - await tester.pumpAndSettle(); - expect(find.text('Custom Overlay Text'), findsNothing); - - // 2. enqueueBanner - Browser.enqueueBanner( - context, - (dismiss) => const Text('Enqueued Banner Text', textDirection: TextDirection.ltr), - ); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); - expect(find.text('Enqueued Banner Text'), findsOneWidget); - - // Dismiss banner - OverlayManager.of(context)?.removeActual(); - await tester.pumpAndSettle(); - expect(find.text('Enqueued Banner Text'), findsNothing); - - // 3. showModalBottomSheet - unawaited( - Browser.showModalBottomSheet( - context: context, - backgroundColor: const Color(0xFFFFFFFF), - builder: (ctx) => const Text('Bottom Sheet Content', textDirection: TextDirection.ltr), - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Bottom Sheet Content'), findsOneWidget); - - // Pop bottom sheet - final sheetContext = tester.element(find.text('Bottom Sheet Content')); - unawaited(sheetContext.pop()); - await tester.pumpAndSettle(); - expect(find.text('Bottom Sheet Content'), findsNothing); - }); + ]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) => WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + onGenerateInitialRoutes: (path) => [ + generate( + RouteSettings( + name: path, + arguments: const {}, + ), + ), + ], + ), + ), + ); + await tester.pumpAndSettle(); + + final context = tester.element(find.text('Home Screen')); + + // 1. showOverlay & dismissOverlay + Browser.showOverlay( + context, + backgroundColor: const Color(0x80000000), + isDismissible: true, + builder: (dismiss) => const Text( + 'Custom Overlay Text', + textDirection: TextDirection.ltr, + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Custom Overlay Text'), findsOneWidget); + + Browser.dismissOverlay(context); + await tester.pumpAndSettle(); + expect(find.text('Custom Overlay Text'), findsNothing); + + // 2. enqueueBanner + Browser.enqueueBanner( + context, + (dismiss) => const Text( + 'Enqueued Banner Text', + textDirection: TextDirection.ltr, + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); + expect(find.text('Enqueued Banner Text'), findsOneWidget); + + // Dismiss banner + OverlayManager.of(context)?.removeActual(); + await tester.pumpAndSettle(); + expect(find.text('Enqueued Banner Text'), findsNothing); + + // 3. showModalBottomSheet + unawaited( + Browser.showModalBottomSheet( + context: context, + backgroundColor: const Color(0xFFFFFFFF), + builder: (ctx) => const Text( + 'Bottom Sheet Content', + textDirection: TextDirection.ltr, + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Bottom Sheet Content'), findsOneWidget); + + // Pop bottom sheet + final sheetContext = tester.element(find.text('Bottom Sheet Content')); + unawaited(sheetContext.pop()); + await tester.pumpAndSettle(); + expect(find.text('Bottom Sheet Content'), findsNothing); + }, + ); }); } diff --git a/test/deferred_browser_route_test.dart b/test/deferred_browser_route_test.dart index 9639a9e..d1a7756 100644 --- a/test/deferred_browser_route_test.dart +++ b/test/deferred_browser_route_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; class _TestPageRoute extends PageRoute { new({required this.child, required RouteSettings settings}) - : super(settings: settings); + : super(settings: settings); final Widget child; @@ -53,69 +53,65 @@ void main() { } testWidgets( - 'shows loading widget then page when future completes successfully', - (tester) async { - var libraryLoaded = false; - final completer = Completer(); - - final route = createDeferredRoute( - loadPageLibrary: () async { - await completer.future; - libraryLoaded = true; - }, - page: const Text( - 'Deferred Page Content', - textDirection: TextDirection.ltr, - ), - onLoading: const Text('Loading...', textDirection: TextDirection.ltr), - ); - - await tester.pumpWidget( - WidgetsApp( - home: Navigator( - onGenerateRoute: (settings) { - if (settings.name == route.path) { - return _TestPageRoute(child: route.page, settings: settings); - } - return _TestPageRoute( - child: const Text( - 'Initial Page', - textDirection: TextDirection.ltr, - ), - settings: settings, - ); - }, - initialRoute: route.path, + 'shows loading widget then page when future completes successfully', + (tester) async { + var libraryLoaded = false; + final completer = Completer(); + + final route = createDeferredRoute( + loadPageLibrary: () async { + await completer.future; + libraryLoaded = true; + }, + page: const Text( + 'Deferred Page Content', + textDirection: TextDirection.ltr, ), - pageRouteBuilder: - (settings, builder) => - PageRouteBuilder( - settings: settings, - pageBuilder: ( - context, - animation, - secondaryAnimation, - ) => - builder(context), + onLoading: const Text('Loading...', textDirection: TextDirection.ltr), + ); + + await tester.pumpWidget( + WidgetsApp( + home: Navigator( + onGenerateRoute: (settings) { + if (settings.name == route.path) { + return _TestPageRoute(child: route.page, settings: settings); + } + return _TestPageRoute( + child: const Text( + 'Initial Page', + textDirection: TextDirection.ltr, + ), + settings: settings, + ); + }, + initialRoute: route.path, + ), + pageRouteBuilder: (settings, builder) => PageRouteBuilder( + settings: settings, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + ), + color: const Color(0xFFFFFFFF), ), - color: const Color(0xFFFFFFFF), - ), - ); + ); - expect(find.text('Loading...'), findsOneWidget); - expect(find.text('Deferred Page Content'), findsNothing); - expect(libraryLoaded, isFalse); + expect(find.text('Loading...'), findsOneWidget); + expect(find.text('Deferred Page Content'), findsNothing); + expect(libraryLoaded, isFalse); - completer.complete(); - await tester.pumpAndSettle(); + completer.complete(); + await tester.pumpAndSettle(); - expect(find.text('Loading...'), findsNothing); - expect(find.text('Deferred Page Content'), findsOneWidget); - expect(libraryLoaded, isTrue); - }); + expect(find.text('Loading...'), findsNothing); + expect(find.text('Deferred Page Content'), findsOneWidget); + expect(libraryLoaded, isTrue); + }, + ); - testWidgets('shows error widget when future completes with an error', - (tester) async { + testWidgets('shows error widget when future completes with an error', ( + tester, + ) async { final completer = Completer(); final testError = Exception('Failed to load library'); @@ -150,15 +146,9 @@ void main() { }, initialRoute: route.path, ), - pageRouteBuilder: - (settings, builder) => - PageRouteBuilder( + pageRouteBuilder: (settings, builder) => PageRouteBuilder( settings: settings, - pageBuilder: ( - context, - animation, - secondaryAnimation, - ) => + pageBuilder: (context, animation, secondaryAnimation) => builder(context), ), color: const Color(0xFFFFFFFF), @@ -183,18 +173,14 @@ void main() { ); }); - test( - 'toCompletedBrowserRoute loads content and returns synchronous BrowserRoute', - () async { + test('toCompletedBrowserRoute loads content and returns synchronous BrowserRoute', () async { var libraryLoaded = false; var serviceLocatorInitialized = false; final deferredRoute = DeferredBrowserRoute( path: '/deferred_sync', loadPageLibrary: () async { - await Future.delayed( - const Duration(milliseconds: 10), - ); + await Future.delayed(const Duration(milliseconds: 10)); libraryLoaded = true; }, pageBuilder: () => const Text( @@ -202,9 +188,7 @@ void main() { textDirection: TextDirection.ltr, ), initializeServiceLocator: () async { - await Future.delayed( - const Duration(milliseconds: 5), - ); + await Future.delayed(const Duration(milliseconds: 5)); serviceLocatorInitialized = true; }, ); @@ -217,10 +201,7 @@ void main() { expect(completedRoute, isA()); expect(completedRoute.path, '/deferred_sync'); - expect( - completedRoute.page, - isA(), - ); + expect(completedRoute.page, isA()); final textWidget = completedRoute.page as Text; expect(textWidget.data, 'Synchronous Page Content'); }); diff --git a/test/modern_route_transitions_integration_test.dart b/test/modern_route_transitions_integration_test.dart index d532bf8..040c49c 100644 --- a/test/modern_route_transitions_integration_test.dart +++ b/test/modern_route_transitions_integration_test.dart @@ -8,10 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('Browser.defaultAdaptiveTransition Tests', () { test('returns platform-appropriate transitions', () { - const sampleRoute = BrowserRoute( - path: '/test', - page: SizedBox(), - ); + const sampleRoute = BrowserRoute(path: '/test', page: SizedBox()); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; expect( @@ -65,33 +62,78 @@ void main() { ]; for (final transition in modernTransitions) { - testWidgets('BrowserPageRoute builds transition correctly for ${transition.name}', ( - tester, - ) async { - final appRoute = BrowserRoute( - path: '/${transition.name}', - page: Text('Page ${transition.name}', textDirection: TextDirection.ltr), - routeTransition: transition, + testWidgets( + 'BrowserPageRoute builds transition correctly for ${transition.name}', + (tester) async { + final appRoute = BrowserRoute( + path: '/${transition.name}', + page: Text( + 'Page ${transition.name}', + textDirection: TextDirection.ltr, + ), + routeTransition: transition, + ); + const traceRoute = PageTraceRoute(); + final pageRoute = BrowserPageRoute( + appRoute: appRoute, + traceRoute: traceRoute, + settings: RouteSettings(name: '/${transition.name}'), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: Builder( + builder: (context) { + return pageRoute.buildTransitions( + context, + kAlwaysCompleteAnimation, + kAlwaysDismissedAnimation, + const Text('Child', textDirection: TextDirection.ltr), + ); + }, + ), + ), + ), + ); + + expect(find.text('Child'), findsOneWidget); + expect(find.byType(RepaintBoundary), findsWidgets); + }, + ); + } + + testWidgets( + 'BrowserPageRoute bypasses transitions when disableAnimations is true', + (tester) async { + const appRoute = BrowserRoute( + path: '/a11y-disable', + page: Text('Page', textDirection: TextDirection.ltr), + routeTransition: RouteTransition.fade_through, ); const traceRoute = PageTraceRoute(); final pageRoute = BrowserPageRoute( appRoute: appRoute, traceRoute: traceRoute, - settings: RouteSettings(name: '/${transition.name}'), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( - data: const MediaQueryData(), + data: const MediaQueryData(disableAnimations: true), child: Builder( builder: (context) { return pageRoute.buildTransitions( context, kAlwaysCompleteAnimation, kAlwaysDismissedAnimation, - const Text('Child', textDirection: TextDirection.ltr), + const Text( + 'Reduced Motion Child', + textDirection: TextDirection.ltr, + ), ); }, ), @@ -99,144 +141,115 @@ void main() { ), ); - expect(find.text('Child'), findsOneWidget); - expect(find.byType(RepaintBoundary), findsWidgets); - }); - } - - testWidgets('BrowserPageRoute bypasses transitions when disableAnimations is true', ( - tester, - ) async { - const appRoute = BrowserRoute( - path: '/a11y-disable', - page: Text('Page', textDirection: TextDirection.ltr), - routeTransition: RouteTransition.fade_through, - ); - const traceRoute = PageTraceRoute(); - final pageRoute = BrowserPageRoute( - appRoute: appRoute, - traceRoute: traceRoute, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(disableAnimations: true), - child: Builder( - builder: (context) { - return pageRoute.buildTransitions( - context, - kAlwaysCompleteAnimation, - kAlwaysDismissedAnimation, - const Text('Reduced Motion Child', textDirection: TextDirection.ltr), - ); - }, - ), - ), - ), - ); - - expect(find.text('Reduced Motion Child'), findsOneWidget); - }); - - testWidgets('BrowserPageRoute bypasses transitions when accessibleNavigation is true', ( - tester, - ) async { - const appRoute = BrowserRoute( - path: '/a11y-nav', - page: Text('Page', textDirection: TextDirection.ltr), - routeTransition: RouteTransition.slide_cupertino, - ); - const traceRoute = PageTraceRoute(); - final pageRoute = BrowserPageRoute( - appRoute: appRoute, - traceRoute: traceRoute, - ); + expect(find.text('Reduced Motion Child'), findsOneWidget); + }, + ); + + testWidgets( + 'BrowserPageRoute bypasses transitions when accessibleNavigation is true', + (tester) async { + const appRoute = BrowserRoute( + path: '/a11y-nav', + page: Text('Page', textDirection: TextDirection.ltr), + routeTransition: RouteTransition.slide_cupertino, + ); + const traceRoute = PageTraceRoute(); + final pageRoute = BrowserPageRoute( + appRoute: appRoute, + traceRoute: traceRoute, + ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(accessibleNavigation: true), - child: Builder( - builder: (context) { - return pageRoute.buildTransitions( - context, - kAlwaysCompleteAnimation, - kAlwaysDismissedAnimation, - const Text('A11y Nav Child', textDirection: TextDirection.ltr), - ); - }, + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(accessibleNavigation: true), + child: Builder( + builder: (context) { + return pageRoute.buildTransitions( + context, + kAlwaysCompleteAnimation, + kAlwaysDismissedAnimation, + const Text( + 'A11y Nav Child', + textDirection: TextDirection.ltr, + ), + ); + }, + ), ), ), - ), - ); + ); - expect(find.text('A11y Nav Child'), findsOneWidget); - }); + expect(find.text('A11y Nav Child'), findsOneWidget); + }, + ); }); group('Modern Transitions Integration in BrowserPopupRoute', () { - testWidgets('BrowserPopupRoute builds transition and respects reduced motion', ( - tester, - ) async { - const appRoute = BrowserRoute( - path: '/popup-modern', - page: Text('Popup Page', textDirection: TextDirection.ltr), - routeTransition: RouteTransition.fade_scale, - ); - const traceRoute = PopupTraceRoute(); - final popupRoute = BrowserPopupRoute( - appRoute: appRoute, - traceRoute: traceRoute, - ); + testWidgets( + 'BrowserPopupRoute builds transition and respects reduced motion', + (tester) async { + const appRoute = BrowserRoute( + path: '/popup-modern', + page: Text('Popup Page', textDirection: TextDirection.ltr), + routeTransition: RouteTransition.fade_scale, + ); + const traceRoute = PopupTraceRoute(); + final popupRoute = BrowserPopupRoute( + appRoute: appRoute, + traceRoute: traceRoute, + ); - // Normal animation - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: Builder( - builder: (context) { - return popupRoute.buildTransitions( - context, - kAlwaysCompleteAnimation, - kAlwaysDismissedAnimation, - const Text('Popup Child', textDirection: TextDirection.ltr), - ); - }, + // Normal animation + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: Builder( + builder: (context) { + return popupRoute.buildTransitions( + context, + kAlwaysCompleteAnimation, + kAlwaysDismissedAnimation, + const Text('Popup Child', textDirection: TextDirection.ltr), + ); + }, + ), ), ), - ), - ); + ); - expect(find.text('Popup Child'), findsOneWidget); - expect(find.byType(RepaintBoundary), findsWidgets); + expect(find.text('Popup Child'), findsOneWidget); + expect(find.byType(RepaintBoundary), findsWidgets); - // Reduced motion - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(disableAnimations: true), - child: Builder( - builder: (context) { - return popupRoute.buildTransitions( - context, - kAlwaysCompleteAnimation, - kAlwaysDismissedAnimation, - const Text('Popup Reduced Motion', textDirection: TextDirection.ltr), - ); - }, + // Reduced motion + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(disableAnimations: true), + child: Builder( + builder: (context) { + return popupRoute.buildTransitions( + context, + kAlwaysCompleteAnimation, + kAlwaysDismissedAnimation, + const Text( + 'Popup Reduced Motion', + textDirection: TextDirection.ltr, + ), + ); + }, + ), ), ), - ), - ); + ); - expect(find.text('Popup Reduced Motion'), findsOneWidget); - }); + expect(find.text('Popup Reduced Motion'), findsOneWidget); + }, + ); }); group('Custom Transition Tests', () { @@ -247,19 +260,14 @@ void main() { final appRoute = BrowserRoute( path: '/custom-app', page: const Text('Custom Page', textDirection: TextDirection.ltr), - customTransition: CustomBuildTransition( - ({ - required animation, - required secondaryAnimation, - required child, - }) { - customBuilderCalled = true; - return Padding( - padding: const EdgeInsets.all(8), - child: child, - ); - }, - ), + customTransition: CustomBuildTransition(({ + required animation, + required secondaryAnimation, + required child, + }) { + customBuilderCalled = true; + return Padding(padding: const EdgeInsets.all(8), child: child); + }), ); const traceRoute = PageTraceRoute(); final pageRoute = BrowserPageRoute( @@ -278,7 +286,10 @@ void main() { context, kAlwaysCompleteAnimation, kAlwaysDismissedAnimation, - const Text('Custom App Child', textDirection: TextDirection.ltr), + const Text( + 'Custom App Child', + textDirection: TextDirection.ltr, + ), ); }, ), @@ -291,29 +302,26 @@ void main() { expect(find.byType(Padding), findsOneWidget); }); - testWidgets('BrowserPageRoute gives precedence to TraceRoute customTransition', ( - tester, - ) async { - var appRouteBuilderCalled = false; - var traceRouteBuilderCalled = false; + testWidgets( + 'BrowserPageRoute gives precedence to TraceRoute customTransition', + (tester) async { + var appRouteBuilderCalled = false; + var traceRouteBuilderCalled = false; - final appRoute = BrowserRoute( - path: '/precedence', - page: const Text('Page', textDirection: TextDirection.ltr), - customTransition: CustomBuildTransition( - ({ + final appRoute = BrowserRoute( + path: '/precedence', + page: const Text('Page', textDirection: TextDirection.ltr), + customTransition: CustomBuildTransition(({ required animation, required secondaryAnimation, required child, }) { appRouteBuilderCalled = true; return child; - }, - ), - ); - final traceRoute = PageTraceRoute( - customTransition: CustomBuildTransition( - ({ + }), + ); + final traceRoute = PageTraceRoute( + customTransition: CustomBuildTransition(({ required animation, required secondaryAnimation, required child, @@ -323,45 +331,44 @@ void main() { decoration: const BoxDecoration(), child: child, ); - }, - ), - ); - final pageRoute = BrowserPageRoute( - appRoute: appRoute, - traceRoute: traceRoute, - ); + }), + ); + final pageRoute = BrowserPageRoute( + appRoute: appRoute, + traceRoute: traceRoute, + ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: Builder( - builder: (context) { - return pageRoute.buildTransitions( - context, - kAlwaysCompleteAnimation, - kAlwaysDismissedAnimation, - const Text('Precedence Child', textDirection: TextDirection.ltr), - ); - }, + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: Builder( + builder: (context) { + return pageRoute.buildTransitions( + context, + kAlwaysCompleteAnimation, + kAlwaysDismissedAnimation, + const Text( + 'Precedence Child', + textDirection: TextDirection.ltr, + ), + ); + }, + ), ), ), - ), - ); + ); - expect(traceRouteBuilderCalled, isTrue); - expect(appRouteBuilderCalled, isFalse); - expect(find.byType(DecoratedBox), findsOneWidget); - }); + expect(traceRouteBuilderCalled, isTrue); + expect(appRouteBuilderCalled, isFalse); + expect(find.byType(DecoratedBox), findsOneWidget); + }, + ); test('TraceRoute subclasses accept customTransition parameter', () { final customBuilder = CustomBuildTransition( - ({ - required animation, - required secondaryAnimation, - required child, - }) => + ({required animation, required secondaryAnimation, required child}) => child, ); @@ -389,19 +396,11 @@ void main() { test('BrowserRoute copyWith preserves or updates customTransition', () { final custom1 = CustomBuildTransition( - ({ - required animation, - required secondaryAnimation, - required child, - }) => + ({required animation, required secondaryAnimation, required child}) => child, ); final custom2 = CustomBuildTransition( - ({ - required animation, - required secondaryAnimation, - required child, - }) => + ({required animation, required secondaryAnimation, required child}) => child, ); @@ -420,4 +419,85 @@ void main() { expect(route3.path, equals('/r3')); }); }); + + group('Modern Transitions Push and Pop Navigation Tests', () { + for (final transition in [ + RouteTransition.shared_axis_x, + RouteTransition.shared_axis_y, + RouteTransition.shared_axis_z, + RouteTransition.fade_scale, + RouteTransition.scale, + RouteTransition.slide_cupertino, + ]) { + testWidgets( + 'Push and Pop with ${transition.name} keeps underlying screen visible during pop', + (tester) async { + final routes = [ + BrowserRoute( + path: '/home', + page: const Text( + 'Home Screen Page', + textDirection: TextDirection.ltr, + ), + routeTransition: transition, + ), + BrowserRoute( + path: '/detail', + page: const Text( + 'Detail Screen Page', + textDirection: TextDirection.ltr, + ), + routeTransition: transition, + ), + ]; + + final browser = Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) { + return WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + initialRoute: '/home', + ); + }, + ); + + await tester.pumpWidget(browser); + await tester.pumpAndSettle(); + + expect(find.text('Home Screen Page'), findsOneWidget); + + // Push detail screen + tester + .state(find.byType(Navigator)) + .pushNamed('/detail'); + + // Halfway through push + await tester.pump(const Duration(milliseconds: 150)); + expect(find.text('Detail Screen Page'), findsOneWidget); + expect(find.text('Home Screen Page'), findsOneWidget); + + // Settle push + await tester.pumpAndSettle(); + expect(find.text('Detail Screen Page'), findsOneWidget); + + // Pop detail screen + tester.state(find.byType(Navigator)).pop(); + + // Halfway through pop + await tester.pump(const Duration(milliseconds: 150)); + // Crucial check: Home screen MUST be present and visible during pop transition + expect(find.text('Home Screen Page'), findsOneWidget); + expect(find.text('Detail Screen Page'), findsOneWidget); + + // Settle pop + await tester.pumpAndSettle(); + expect(find.text('Home Screen Page'), findsOneWidget); + expect(find.text('Detail Screen Page'), findsNothing); + }, + ); + } + }); } diff --git a/test/modern_transitions_test.dart b/test/modern_transitions_test.dart index a4882c0..ce436eb 100644 --- a/test/modern_transitions_test.dart +++ b/test/modern_transitions_test.dart @@ -143,7 +143,10 @@ void main() { ), ); - expect(find.text('SharedAxis ${type.name} ${direction.name}'), findsOneWidget); + expect( + find.text('SharedAxis ${type.name} ${direction.name}'), + findsOneWidget, + ); primaryController.value = 0.0; await tester.pump(); @@ -188,7 +191,9 @@ void main() { animation: primaryController, secondaryAnimation: secondaryController, hasShadow: hasShadow, - child: Text('SlideCupertino shadow=$hasShadow dir=${direction.name}'), + child: Text( + 'SlideCupertino shadow=$hasShadow dir=${direction.name}', + ), ); }, ), @@ -216,9 +221,7 @@ void main() { } }); - testWidgets('Scale renders scale and fade', ( - tester, - ) async { + testWidgets('Scale renders scale and fade', (tester) async { late AnimationController primaryController; late AnimationController secondaryController; @@ -329,11 +332,7 @@ void main() { required animation, required secondaryAnimation, required child, - }) => - Opacity( - opacity: 0.5, - child: child, - ), + }) => Opacity(opacity: 0.5, child: child), ).call( animation: dummyAnim, secondaryAnimation: dummySecAnim, @@ -350,12 +349,27 @@ void main() { }); test('RouteTransition enum contains all modern transitions and correctly maps to BuildTransitions', () { - expect(RouteTransition.fade_through.build, isA()); + expect( + RouteTransition.fade_through.build, + isA(), + ); expect(RouteTransition.fade_scale.build, isA()); - expect(RouteTransition.shared_axis_x.build, isA()); - expect(RouteTransition.shared_axis_y.build, isA()); - expect(RouteTransition.shared_axis_z.build, isA()); - expect(RouteTransition.slide_cupertino.build, isA()); + expect( + RouteTransition.shared_axis_x.build, + isA(), + ); + expect( + RouteTransition.shared_axis_y.build, + isA(), + ); + expect( + RouteTransition.shared_axis_z.build, + isA(), + ); + expect( + RouteTransition.slide_cupertino.build, + isA(), + ); expect(RouteTransition.scale.build, isA()); // Legacy transitions are preserved diff --git a/test/navigator_extension_test.dart b/test/navigator_extension_test.dart index 08a4fef..03f5ae3 100644 --- a/test/navigator_extension_test.dart +++ b/test/navigator_extension_test.dart @@ -19,7 +19,8 @@ Widget createNavigationApp({ BrowserRoute? defaultRoute, Future Function(Uri)? openUrl, }) { - final effectiveRoutes = routes ?? + final effectiveRoutes = + routes ?? [ const BrowserRoute( path: '/home', @@ -73,10 +74,7 @@ void main() { final context = tester.element(find.text('Home Screen')); unawaited( - context.pushNamed( - '/detail', - args: [const TestUserArgs('user_42')], - ), + context.pushNamed('/detail', args: [const TestUserArgs('user_42')]), ); await tester.pumpAndSettle(); @@ -127,28 +125,32 @@ void main() { expect(find.text('Target Screen'), findsOneWidget); final targetContext = tester.element(find.text('Target Screen')); - expect(targetContext.getArgument()?.id, equals('staged_push')); + expect( + targetContext.getArgument()?.id, + equals('staged_push'), + ); }); - testWidgets('pop sends return arguments and updates previous route settings', ( - tester, - ) async { - await tester.pumpWidget(createNavigationApp()); - await tester.pumpAndSettle(); + testWidgets( + 'pop sends return arguments and updates previous route settings', + (tester) async { + await tester.pumpWidget(createNavigationApp()); + await tester.pumpAndSettle(); - final homeContext = tester.element(find.text('Home Screen')); - unawaited(homeContext.pushNamed('/detail')); - await tester.pumpAndSettle(); + final homeContext = tester.element(find.text('Home Screen')); + unawaited(homeContext.pushNamed('/detail')); + await tester.pumpAndSettle(); - final detailContext = tester.element(find.text('Detail Screen')); - await detailContext.pop(args: const TestResultArgs('success_result')); - await tester.pumpAndSettle(); + final detailContext = tester.element(find.text('Detail Screen')); + await detailContext.pop(args: const TestResultArgs('success_result')); + await tester.pumpAndSettle(); - expect(find.text('Home Screen'), findsOneWidget); - final currentHomeContext = tester.element(find.text('Home Screen')); - final result = currentHomeContext.getArgument(); - expect(result?.value, equals('success_result')); - }); + expect(find.text('Home Screen'), findsOneWidget); + final currentHomeContext = tester.element(find.text('Home Screen')); + final result = currentHomeContext.getArgument(); + expect(result?.value, equals('success_result')); + }, + ); testWidgets('pop falls back to defaultRoute when canPop is false', ( tester, @@ -207,9 +209,7 @@ void main() { await tester.pumpAndSettle(); final step2Context = tester.element(find.text('Step 2 Screen')); - await step2Context.popToFirst( - args: [const TestResultArgs('from_lvl2')], - ); + await step2Context.popToFirst(args: [const TestResultArgs('from_lvl2')]); await tester.pumpAndSettle(); expect(find.text('Home Screen'), findsOneWidget); @@ -231,9 +231,7 @@ void main() { final homeContext = tester.element(find.text('Home Screen')); unawaited( - homeContext.popToFirst( - args: [const TestResultArgs('root_pop_first')], - ), + homeContext.popToFirst(args: [const TestResultArgs('root_pop_first')]), ); await tester.pumpAndSettle(); @@ -245,39 +243,40 @@ void main() { ); }); - testWidgets('popToFirstAndPushNamed pops to first and pushes target route', ( - tester, - ) async { - await tester.pumpWidget(createNavigationApp()); - await tester.pumpAndSettle(); + testWidgets( + 'popToFirstAndPushNamed pops to first and pushes target route', + (tester) async { + await tester.pumpWidget(createNavigationApp()); + await tester.pumpAndSettle(); - final homeContext = tester.element(find.text('Home Screen')); - unawaited(homeContext.pushNamed('/step1')); - await tester.pumpAndSettle(); + final homeContext = tester.element(find.text('Home Screen')); + unawaited(homeContext.pushNamed('/step1')); + await tester.pumpAndSettle(); - final step1Context = tester.element(find.text('Step 1 Screen')); - unawaited(step1Context.pushNamed('/step2')); - await tester.pumpAndSettle(); + final step1Context = tester.element(find.text('Step 1 Screen')); + unawaited(step1Context.pushNamed('/step2')); + await tester.pumpAndSettle(); - final step2Context = tester.element(find.text('Step 2 Screen')); - unawaited( - step2Context.popToFirstAndPushNamed( - '/target', - args: [const TestUserArgs('direct_target')], - ), - ); - await tester.pumpAndSettle(); + final step2Context = tester.element(find.text('Step 2 Screen')); + unawaited( + step2Context.popToFirstAndPushNamed( + '/target', + args: [const TestUserArgs('direct_target')], + ), + ); + await tester.pumpAndSettle(); - expect(find.text('Target Screen'), findsOneWidget); - expect(find.text('Step 1 Screen'), findsNothing); - expect(find.text('Step 2 Screen'), findsNothing); + expect(find.text('Target Screen'), findsOneWidget); + expect(find.text('Step 1 Screen'), findsNothing); + expect(find.text('Step 2 Screen'), findsNothing); - final targetContext = tester.element(find.text('Target Screen')); - expect( - targetContext.getArgument()?.id, - equals('direct_target'), - ); - }); + final targetContext = tester.element(find.text('Target Screen')); + expect( + targetContext.getArgument()?.id, + equals('direct_target'), + ); + }, + ); testWidgets('popToFirstAndPushReplacementNamed replaces the first route', ( tester, @@ -366,17 +365,18 @@ void main() { expect(detailContext.getArgument(), isNull); }); - testWidgets('canNavigate returns true for registered paths and false otherwise', ( - tester, - ) async { - await tester.pumpWidget(createNavigationApp()); - await tester.pumpAndSettle(); + testWidgets( + 'canNavigate returns true for registered paths and false otherwise', + (tester) async { + await tester.pumpWidget(createNavigationApp()); + await tester.pumpAndSettle(); - final context = tester.element(find.text('Home Screen')); - expect(context.canNavigate('/home'), isTrue); - expect(context.canNavigate('/detail'), isTrue); - expect(context.canNavigate('/unknown_route'), isFalse); - }); + final context = tester.element(find.text('Home Screen')); + expect(context.canNavigate('/home'), isTrue); + expect(context.canNavigate('/detail'), isTrue); + expect(context.canNavigate('/unknown_route'), isFalse); + }, + ); testWidgets('openUrl invokes configured callback in BrowserConfig', ( tester, @@ -391,54 +391,65 @@ void main() { ); await tester.pumpAndSettle(); - tester.element(find.text('Home Screen')).openUrl(Uri.parse('https://flutter.dev')); + tester + .element(find.text('Home Screen')) + .openUrl(Uri.parse('https://flutter.dev')); expect(openedUri, equals(Uri.parse('https://flutter.dev'))); }); - testWidgets('launchAction handles external links, internal routes and navigateType', ( - tester, - ) async { - Uri? openedUri; - await tester.pumpWidget( - createNavigationApp( - openUrl: (uri) async { - openedUri = uri; - }, - ), - ); - await tester.pumpAndSettle(); + testWidgets( + 'launchAction handles external links, internal routes and navigateType', + (tester) async { + Uri? openedUri; + await tester.pumpWidget( + createNavigationApp( + openUrl: (uri) async { + openedUri = uri; + }, + ), + ); + await tester.pumpAndSettle(); - // 1. External URL - tester.element(find.text('Home Screen')).launchAction('https://dart.dev'); - expect(openedUri, equals(Uri.parse('https://dart.dev'))); + // 1. External URL + tester + .element(find.text('Home Screen')) + .launchAction('https://dart.dev'); + expect(openedUri, equals(Uri.parse('https://dart.dev'))); - // 2. Default push - tester.element(find.text('Home Screen')).launchAction('/detail'); - await tester.pumpAndSettle(); - expect(find.text('Detail Screen'), findsOneWidget); + // 2. Default push + tester.element(find.text('Home Screen')).launchAction('/detail'); + await tester.pumpAndSettle(); + expect(find.text('Detail Screen'), findsOneWidget); - // 3. navigateType=pop - tester.element(find.text('Detail Screen')).launchAction('/?navigateType=pop'); - await tester.pumpAndSettle(); - expect(find.text('Home Screen'), findsOneWidget); + // 3. navigateType=pop + tester + .element(find.text('Detail Screen')) + .launchAction('/?navigateType=pop'); + await tester.pumpAndSettle(); + expect(find.text('Home Screen'), findsOneWidget); - // 4. navigateType=pushreplacement - tester.element(find.text('Home Screen')).launchAction('/step1?navigateType=pushreplacement'); - await tester.pumpAndSettle(); - expect(find.text('Step 1 Screen'), findsOneWidget); - expect(find.text('Home Screen'), findsNothing); + // 4. navigateType=pushreplacement + tester + .element(find.text('Home Screen')) + .launchAction('/step1?navigateType=pushreplacement'); + await tester.pumpAndSettle(); + expect(find.text('Step 1 Screen'), findsOneWidget); + expect(find.text('Home Screen'), findsNothing); - // 5. navigateType=popfirstandpush - tester.element(find.text('Step 1 Screen')).launchAction('/step2?navigateType=popfirstandpush'); - await tester.pumpAndSettle(); - expect(find.text('Step 2 Screen'), findsOneWidget); + // 5. navigateType=popfirstandpush + tester + .element(find.text('Step 1 Screen')) + .launchAction('/step2?navigateType=popfirstandpush'); + await tester.pumpAndSettle(); + expect(find.text('Step 2 Screen'), findsOneWidget); - // 6. Invalid or non-navigable action - tester.element(find.text('Step 2 Screen')) - ..launchAction(':::invalid_uri') - ..launchAction('/non_existent_route'); - await tester.pumpAndSettle(); - expect(find.text('Step 2 Screen'), findsOneWidget); - }); + // 6. Invalid or non-navigable action + tester.element(find.text('Step 2 Screen')) + ..launchAction(':::invalid_uri') + ..launchAction('/non_existent_route'); + await tester.pumpAndSettle(); + expect(find.text('Step 2 Screen'), findsOneWidget); + }, + ); }); } diff --git a/test/overlay_manager_test.dart b/test/overlay_manager_test.dart index 3c8c36f..aadc49b 100644 --- a/test/overlay_manager_test.dart +++ b/test/overlay_manager_test.dart @@ -39,8 +39,9 @@ void main() { expect(foundState, equals(foundRootState)); }); - testWidgets('showModal and dismissModal manage overlay visibility', - (tester) async { + testWidgets('showModal and dismissModal manage overlay visibility', ( + tester, + ) async { late BuildContext savedContext; await tester.pumpWidget( @@ -69,10 +70,7 @@ void main() { content: (remove) => Column( children: [ const Text('Modal Content'), - ElevatedButton( - onPressed: remove, - child: const Text('Close Modal'), - ), + ElevatedButton(onPressed: remove, child: const Text('Close Modal')), ], ), transition: const OverlayTraceRoute( @@ -143,8 +141,9 @@ void main() { expect(find.text('Banner 1 Content'), findsNothing); }); - testWidgets('Disposing OverlayManager cleans up without errors', - (tester) async { + testWidgets('Disposing OverlayManager cleans up without errors', ( + tester, + ) async { final showManager = ValueNotifier(true); await tester.pumpWidget( @@ -197,123 +196,125 @@ void main() { expect(find.text('Overlay'), findsNothing); }); - testWidgets('enqueueBanner displays banner and allows clicks and interaction on underlying widgets', ( - tester, - ) async { - var underlyingButtonClicked = false; - - await tester.pumpWidget( - MaterialApp( - home: OverlayManager( - child: Scaffold( - body: Builder( - builder: (context) { - return Column( - children: [ - ElevatedButton( - onPressed: () { - OverlayManager.enqueueBanner( - context, - content: (remove) => const ColoredBox( - color: Colors.amber, - child: Text('Top Banner Content'), - ), - ); - }, - child: const Text('Show Banner'), - ), - const SizedBox(height: 100), - ElevatedButton( - onPressed: () { - underlyingButtonClicked = true; - }, - child: const Text('Underlying Button'), - ), - ], - ); - }, + testWidgets( + 'enqueueBanner displays banner and allows clicks and interaction on underlying widgets', + (tester) async { + var underlyingButtonClicked = false; + + await tester.pumpWidget( + MaterialApp( + home: OverlayManager( + child: Scaffold( + body: Builder( + builder: (context) { + return Column( + children: [ + ElevatedButton( + onPressed: () { + OverlayManager.enqueueBanner( + context, + content: (remove) => const ColoredBox( + color: Colors.amber, + child: Text('Top Banner Content'), + ), + ); + }, + child: const Text('Show Banner'), + ), + const SizedBox(height: 100), + ElevatedButton( + onPressed: () { + underlyingButtonClicked = true; + }, + child: const Text('Underlying Button'), + ), + ], + ); + }, + ), ), ), ), - ), - ); - - // Trigger banner - await tester.tap(find.text('Show Banner')); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); - - expect(find.text('Top Banner Content'), findsOneWidget); - - // Click underlying button while banner is visible - await tester.tap(find.text('Underlying Button')); - await tester.pump(); - - expect(underlyingButtonClicked, isTrue); - - // Settle timer - await tester.pump(const Duration(seconds: 6)); - await tester.pump(const Duration(milliseconds: 350)); - }); - - testWidgets('enqueueBanner with standard Flutter buttons works out of the box without special configuration', ( - tester, - ) async { - var bannerActionClicked = false; - - await tester.pumpWidget( - MaterialApp( - home: OverlayManager( - child: Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - OverlayManager.enqueueBanner( - context, - duration: Duration.zero, - content: (remove) => Container( - color: Colors.blue, - padding: const EdgeInsets.all(12), - child: Row( - children: [ - const Text('Notification'), - ElevatedButton( - onPressed: () { - bannerActionClicked = true; - remove(); - }, - child: const Text('Action'), - ), - ], + ); + + // Trigger banner + await tester.tap(find.text('Show Banner')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); + + expect(find.text('Top Banner Content'), findsOneWidget); + + // Click underlying button while banner is visible + await tester.tap(find.text('Underlying Button')); + await tester.pump(); + + expect(underlyingButtonClicked, isTrue); + + // Settle timer + await tester.pump(const Duration(seconds: 6)); + await tester.pump(const Duration(milliseconds: 350)); + }, + ); + + testWidgets( + 'enqueueBanner with standard Flutter buttons works out of the box without special configuration', + (tester) async { + var bannerActionClicked = false; + + await tester.pumpWidget( + MaterialApp( + home: OverlayManager( + child: Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + OverlayManager.enqueueBanner( + context, + duration: Duration.zero, + content: (remove) => Container( + color: Colors.blue, + padding: const EdgeInsets.all(12), + child: Row( + children: [ + const Text('Notification'), + ElevatedButton( + onPressed: () { + bannerActionClicked = true; + remove(); + }, + child: const Text('Action'), + ), + ], + ), ), - ), - ); - }, - child: const Text('Show Persistent Banner'), - ); - }, + ); + }, + child: const Text('Show Persistent Banner'), + ); + }, + ), ), ), ), - ), - ); + ); - // Open banner - await tester.tap(find.text('Show Persistent Banner')); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); + // Open banner + await tester.tap(find.text('Show Persistent Banner')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); - expect(find.text('Notification'), findsOneWidget); - expect(find.text('Action'), findsOneWidget); + expect(find.text('Notification'), findsOneWidget); + expect(find.text('Action'), findsOneWidget); - // Tap standard button in banner - await tester.tap(find.text('Action')); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); + // Tap standard button in banner + await tester.tap(find.text('Action')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 350)); - expect(bannerActionClicked, isTrue); - expect(find.text('Notification'), findsNothing); - }); + expect(bannerActionClicked, isTrue); + expect(find.text('Notification'), findsNothing); + }, + ); }); } diff --git a/test/route_arguments_test.dart b/test/route_arguments_test.dart index de82d26..32ae7f2 100644 --- a/test/route_arguments_test.dart +++ b/test/route_arguments_test.dart @@ -22,8 +22,9 @@ final class SessionArgs extends RouteParams { void main() { group('Route Arguments & Polymorphism Tests', () { - testWidgets('Exact type and polymorphic base class argument retrieval', - (tester) async { + testWidgets('Exact type and polymorphic base class argument retrieval', ( + tester, + ) async { BaseUserArgs? retrievedAsBase; AdminUserArgs? retrievedAsExact; SessionArgs? retrievedSession; @@ -35,10 +36,7 @@ void main() { page: Builder( builder: (context) { savedContext = context; - return const Text( - 'Home', - textDirection: TextDirection.ltr, - ); + return const Text('Home', textDirection: TextDirection.ltr); }, ), ), @@ -105,8 +103,9 @@ void main() { expect(retrievedSession?.token, 'tok_xyz123'); }); - testWidgets('getArgumentAndClean consumes argument on read', - (tester) async { + testWidgets('getArgumentAndClean consumes argument on read', ( + tester, + ) async { SessionArgs? firstRead; SessionArgs? secondRead; late BuildContext savedContext; @@ -163,9 +162,9 @@ void main() { expect(secondRead, isNull); }); - testWidgets( - 'getArgumentAndClean works polymorphically with base class', - (tester) async { + testWidgets('getArgumentAndClean works polymorphically with base class', ( + tester, + ) async { BaseUserArgs? firstReadBase; AdminUserArgs? secondReadExact; late BuildContext savedContext; @@ -224,8 +223,9 @@ void main() { expect(secondReadExact, isNull); }); - testWidgets('Missing argument returns null without throwing', - (tester) async { + testWidgets('Missing argument returns null without throwing', ( + tester, + ) async { SessionArgs? retrieved; final routes = [ @@ -259,59 +259,53 @@ void main() { expect(retrieved, isNull); }); - testWidgets('DeepLinkParam is retrieved correctly from URL query parameters', - (tester) async { - DeepLinkParam? deepLink; - - final routes = [ - BrowserRoute( - path: '/home', - page: Builder( - builder: (context) { - deepLink = context.getArgument(); - return Text( - 'DeepLink: ${deepLink?.params['code']}', - textDirection: TextDirection.ltr, + testWidgets( + 'DeepLinkParam is retrieved correctly from URL query parameters', + (tester) async { + DeepLinkParam? deepLink; + + final routes = [ + BrowserRoute( + path: '/home', + page: Builder( + builder: (context) { + deepLink = context.getArgument(); + return Text( + 'DeepLink: ${deepLink?.params['code']}', + textDirection: TextDirection.ltr, + ); + }, + ), + ), + ]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) { + return WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + initialRoute: '/home?code=12345&source=email', ); }, ), - ), - ]; - - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) { - return WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - initialRoute: '/home?code=12345&source=email', - ); - }, - ), - ); + ); - await tester.pumpAndSettle(); - expect(find.text('DeepLink: 12345'), findsOneWidget); - expect(deepLink, isNotNull); - expect(deepLink?.params['code'], '12345'); - expect(deepLink?.params['source'], 'email'); - }); + await tester.pumpAndSettle(); + expect(find.text('DeepLink: 12345'), findsOneWidget); + expect(deepLink, isNotNull); + expect(deepLink?.params['code'], '12345'); + expect(deepLink?.params['source'], 'email'); + }, + ); - test( - 'generate handles unmodifiable map with TraceRoute without throwing UnsupportedError', - () { + test('generate handles unmodifiable map with TraceRoute without throwing UnsupportedError', () { final routes = [ - const BrowserRoute( - path: '/home', - page: SizedBox(), - ), - const BrowserRoute( - path: '/detail', - page: SizedBox(), - ), + const BrowserRoute(path: '/home', page: SizedBox()), + const BrowserRoute(path: '/detail', page: SizedBox()), ]; final browser = Browser( @@ -327,19 +321,13 @@ void main() { expect( () => browser.generate( - RouteSettings( - name: '/detail', - arguments: unmodifiableArgs, - ), + RouteSettings(name: '/detail', arguments: unmodifiableArgs), ), returnsNormally, ); final route = browser.generate( - RouteSettings( - name: '/detail', - arguments: unmodifiableArgs, - ), + RouteSettings(name: '/detail', arguments: unmodifiableArgs), ); expect(route, isA>()); @@ -348,18 +336,10 @@ void main() { expect(generatedArgs?.containsKey(PageTraceRoute), isFalse); }); - test( - 'generate handles const map without throwing and cleans TraceRoute keys/values', - () { + test('generate handles const map without throwing and cleans TraceRoute keys/values', () { final routes = [ - const BrowserRoute( - path: '/home', - page: SizedBox(), - ), - const BrowserRoute( - path: '/modal', - page: SizedBox(), - ), + const BrowserRoute(path: '/home', page: SizedBox()), + const BrowserRoute(path: '/modal', page: SizedBox()), ]; final browser = Browser( @@ -373,30 +353,22 @@ void main() { }; final route = browser.generate( - const RouteSettings( - name: '/modal', - arguments: constArgs, - ), + const RouteSettings(name: '/modal', arguments: constArgs), ); expect(route, isA>()); final generatedArgs = route.settings.arguments as Map?; expect(generatedArgs?[AdminUserArgs], isA()); - expect((generatedArgs?[AdminUserArgs] as AdminUserArgs?)?.id, 'adm_const'); + expect( + (generatedArgs?[AdminUserArgs] as AdminUserArgs?)?.id, + 'adm_const', + ); }); - test( - 'generate handles unmodifiable map with polymorphic TraceRoute and selects PopupRoute', - () { + test('generate handles unmodifiable map with polymorphic TraceRoute and selects PopupRoute', () { final routes = [ - const BrowserRoute( - path: '/home', - page: SizedBox(), - ), - const BrowserRoute( - path: '/modal', - page: SizedBox(), - ), + const BrowserRoute(path: '/home', page: SizedBox()), + const BrowserRoute(path: '/modal', page: SizedBox()), ]; final browser = Browser( @@ -410,10 +382,7 @@ void main() { }); final route = browser.generate( - RouteSettings( - name: '/modal', - arguments: unmodifiableArgs, - ), + RouteSettings(name: '/modal', arguments: unmodifiableArgs), ); expect(route, isA>()); @@ -422,123 +391,126 @@ void main() { }); testWidgets( - 'Navigation with unmodifiable arguments Map retrieves arguments and does not throw', - (tester) async { - AdminUserArgs? retrievedAdmin; - late BuildContext savedContext; - - final routes = [ - BrowserRoute( - path: '/home', - page: Builder( - builder: (context) { - savedContext = context; - return const Text('Home', textDirection: TextDirection.ltr); - }, + 'Navigation with unmodifiable arguments Map retrieves arguments and does not throw', + (tester) async { + AdminUserArgs? retrievedAdmin; + late BuildContext savedContext; + + final routes = [ + BrowserRoute( + path: '/home', + page: Builder( + builder: (context) { + savedContext = context; + return const Text('Home', textDirection: TextDirection.ltr); + }, + ), ), - ), - BrowserRoute( - path: '/detail', - page: Builder( - builder: (context) { - retrievedAdmin = context.getArgument(); - return Text( - 'Detail: ${retrievedAdmin?.role}', - textDirection: TextDirection.ltr, + BrowserRoute( + path: '/detail', + page: Builder( + builder: (context) { + retrievedAdmin = context.getArgument(); + return Text( + 'Detail: ${retrievedAdmin?.role}', + textDirection: TextDirection.ltr, + ); + }, + ), + ), + ]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) { + return WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + initialRoute: '/home', ); }, ), - ), - ]; + ); - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) { - return WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - initialRoute: '/home', - ); - }, - ), - ); - - await tester.pumpAndSettle(); - expect(find.text('Home'), findsOneWidget); + await tester.pumpAndSettle(); + expect(find.text('Home'), findsOneWidget); - final unmodifiableArgs = Map.unmodifiable({ - PageTraceRoute: TraceRoute.page(), - AdminUserArgs: const AdminUserArgs(id: 'adm_unmodifiable', role: 'editor'), - }); + final unmodifiableArgs = Map.unmodifiable({ + PageTraceRoute: TraceRoute.page(), + AdminUserArgs: const AdminUserArgs( + id: 'adm_unmodifiable', + role: 'editor', + ), + }); - savedContext.navigate.pushNamed( - '/detail', - arguments: unmodifiableArgs, - ); - await tester.pumpAndSettle(); + savedContext.navigate.pushNamed('/detail', arguments: unmodifiableArgs); + await tester.pumpAndSettle(); - expect(find.text('Detail: editor'), findsOneWidget); - expect(retrievedAdmin, isNotNull); - expect(retrievedAdmin?.id, 'adm_unmodifiable'); - expect(retrievedAdmin?.role, 'editor'); - }); + expect(find.text('Detail: editor'), findsOneWidget); + expect(retrievedAdmin, isNotNull); + expect(retrievedAdmin?.id, 'adm_unmodifiable'); + expect(retrievedAdmin?.role, 'editor'); + }, + ); testWidgets( - 'getArgumentAndClean on missing polymorphic argument in unmodifiable map does not throw', - (tester) async { - final routes = [ - const BrowserRoute( - path: '/home', - page: SizedBox(), - ), - ]; - - await tester.pumpWidget( - Browser( - routes: routes, - defaultRoute: routes.first, - builder: (context, routeObserver, generate) { - return WidgetsApp( - color: const Color(0xFFFFFFFF), - navigatorObservers: [routeObserver], - onGenerateRoute: generate, - initialRoute: '/home', - ); - }, - ), - ); - - await tester.pumpAndSettle(); - - final unmodifiableArgs = Map.unmodifiable({ - const AdminUserArgs(id: 'adm_1', role: 'admin').runtimeType: - const AdminUserArgs(id: 'adm_1', role: 'admin'), - }); + 'getArgumentAndClean on missing polymorphic argument in unmodifiable map does not throw', + (tester) async { + final routes = [const BrowserRoute(path: '/home', page: SizedBox())]; + + await tester.pumpWidget( + Browser( + routes: routes, + defaultRoute: routes.first, + builder: (context, routeObserver, generate) { + return WidgetsApp( + color: const Color(0xFFFFFFFF), + navigatorObservers: [routeObserver], + onGenerateRoute: generate, + initialRoute: '/home', + ); + }, + ), + ); - final settings = RouteSettings(name: '/home', arguments: unmodifiableArgs); - final element = tester.element(find.byType(SizedBox)); + await tester.pumpAndSettle(); - expect( - () => element.getArgumentAndClean(settings: settings), - returnsNormally, - ); - expect( - element.getArgumentAndClean(settings: settings), - isNull, - ); - }); + final unmodifiableArgs = Map.unmodifiable({ + const AdminUserArgs(id: 'adm_1', role: 'admin').runtimeType: + const AdminUserArgs(id: 'adm_1', role: 'admin'), + }); - test('DataParam holds generic data and RouteParams defaults validate to true', () { - const dataParam = DataParam(42); - expect(dataParam.data, equals(42)); - expect(dataParam.validate(), isTrue); + final settings = RouteSettings( + name: '/home', + arguments: unmodifiableArgs, + ); + final element = tester.element(find.byType(SizedBox)); + + expect( + () => element.getArgumentAndClean(settings: settings), + returnsNormally, + ); + expect( + element.getArgumentAndClean(settings: settings), + isNull, + ); + }, + ); - const nullDataParam = DataParam(null); - expect(nullDataParam.data, isNull); - expect(nullDataParam.validate(), isTrue); - }); + test( + 'DataParam holds generic data and RouteParams defaults validate to true', + () { + const dataParam = DataParam(42); + expect(dataParam.data, equals(42)); + expect(dataParam.validate(), isTrue); + + const nullDataParam = DataParam(null); + expect(nullDataParam.data, isNull); + expect(nullDataParam.validate(), isTrue); + }, + ); }); } diff --git a/test/route_observer_test.dart b/test/route_observer_test.dart index 1c9b6ca..3782355 100644 --- a/test/route_observer_test.dart +++ b/test/route_observer_test.dart @@ -11,8 +11,9 @@ final class TestArgs extends RouteParams { void main() { group('RouteObserverProvider and Browser.watch Tests', () { - testWidgets('triggers onAppear and onDisappear on push and pop', - (tester) async { + testWidgets('triggers onAppear and onDisappear on push and pop', ( + tester, + ) async { var homeAppearCount = 0; var homeDisappearCount = 0; var detailAppearCount = 0; @@ -89,8 +90,9 @@ void main() { expect(homeAppearCount, 2); }); - testWidgets('Trace navigation helper methods push and pop properly', - (tester) async { + testWidgets('Trace navigation helper methods push and pop properly', ( + tester, + ) async { late BuildContext savedContext; TestArgs? receivedArgs; @@ -148,9 +150,7 @@ void main() { expect(receivedArgs?.value, 'arg1'); // Trace.pushAndReplacement - const trace2 = Trace( - path: '/step2', - ); + const trace2 = Trace(path: '/step2'); unawaited(trace2.pushAndReplacement(savedContext)); await tester.pumpAndSettle(); expect(find.text('Step 2'), findsOneWidget); diff --git a/test/sheet_test.dart b/test/sheet_test.dart index 38d8956..75b152e 100644 --- a/test/sheet_test.dart +++ b/test/sheet_test.dart @@ -27,17 +27,12 @@ class _TestHeader extends ModalBaseHeader { } class _TestModal extends ModalBase<_TestModalParams> { - const new({ - required super.params, - this.onBodyBuilt, - }); + const new({required super.params, this.onBodyBuilt}); final void Function(ChangeDrawerSize? changeDrawerSize)? onBodyBuilt; @override - ModalBaseHeaderParameter contextParameters({ - required BuildContext context, - }) { + ModalBaseHeaderParameter contextParameters({required BuildContext context}) { return const ModalBaseHeaderParameter( background: Colors.white, headerBackground: Colors.blue, @@ -89,10 +84,7 @@ class _TestModal extends ModalBase<_TestModalParams> { } class _TestSheet extends SheetBase<_TestModalParams> { - const new({ - required super.modal, - this.onAdjustSizeCalled, - }); + const new({required super.modal, this.onAdjustSizeCalled}); final VoidCallback? onAdjustSizeCalled; @@ -121,45 +113,41 @@ class _TestSheet extends SheetBase<_TestModalParams> { void main() { group('ModalBase & SheetBase with ChangeDrawerSize', () { - testWidgets('renders modal structure and passes ChangeDrawerSize callback', - (tester) async { - var adjustSizeCalled = false; - ChangeDrawerSize? receivedCallback; - - final modal = _TestModal( - params: _TestModalParams(), - onBodyBuilt: (callback) { - receivedCallback = callback; - }, - ); + testWidgets( + 'renders modal structure and passes ChangeDrawerSize callback', + (tester) async { + var adjustSizeCalled = false; + ChangeDrawerSize? receivedCallback; + + final modal = _TestModal( + params: _TestModalParams(), + onBodyBuilt: (callback) { + receivedCallback = callback; + }, + ); - final sheet = _TestSheet( - modal: modal, - onAdjustSizeCalled: () { - adjustSizeCalled = true; - }, - ); + final sheet = _TestSheet( + modal: modal, + onAdjustSizeCalled: () { + adjustSizeCalled = true; + }, + ); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: sheet, - ), - ), - ); + await tester.pumpWidget(MaterialApp(home: Scaffold(body: sheet))); - expect(find.text('Test Title'), findsOneWidget); - expect(find.text('Modal Body Content'), findsOneWidget); - expect(find.text('Bottom Bar Content'), findsOneWidget); - expect(find.text('Expand Drawer'), findsOneWidget); - expect(receivedCallback, isNotNull); + expect(find.text('Test Title'), findsOneWidget); + expect(find.text('Modal Body Content'), findsOneWidget); + expect(find.text('Bottom Bar Content'), findsOneWidget); + expect(find.text('Expand Drawer'), findsOneWidget); + expect(receivedCallback, isNotNull); - // Tap button to invoke changeDrawerSize({required bool isExpanded}) - await tester.tap(find.text('Expand Drawer')); - await tester.pump(); + // Tap button to invoke changeDrawerSize({required bool isExpanded}) + await tester.tap(find.text('Expand Drawer')); + await tester.pump(); - expect(adjustSizeCalled, isTrue); - }); + expect(adjustSizeCalled, isTrue); + }, + ); test('SafeAreaManager creates proper instances and widgets', () { const manager = SafeAreaManager( @@ -180,7 +168,12 @@ void main() { expect(allManager.bottom, isTrue); expect(allManager.left, isTrue); - const fromLTRBManager = SafeAreaManager.fromLTRB(false, true, false, true); + const fromLTRBManager = SafeAreaManager.fromLTRB( + false, + true, + false, + true, + ); expect(fromLTRBManager.left, isFalse); expect(fromLTRBManager.top, isTrue); expect(fromLTRBManager.right, isFalse); @@ -214,7 +207,10 @@ void main() { expect(standard.expand, isFalse); expect(standard.snap, isTrue); expect(standard.snapSizes, equals([0.5, 0.9])); - expect(standard.snapAnimationDuration, equals(const Duration(milliseconds: 250))); + expect( + standard.snapAnimationDuration, + equals(const Duration(milliseconds: 250)), + ); expect(standard.shouldCloseOnMinExtent, isFalse); const small = ModalDraggableScrollableSheetParams.small(); @@ -294,7 +290,10 @@ void main() { expect(small.minHeightChildSize, equals(0.35)); expect(small.maxHeightChildSize, equals(0.75)); expect(small.snapSizes, equals([0.5])); - expect(small.snapAnimationDuration, equals(const Duration(milliseconds: 200))); + expect( + small.snapAnimationDuration, + equals(const Duration(milliseconds: 200)), + ); expect(small.shouldCloseOnMinExtent, isFalse); expect(small.expand, isFalse); expect(small.snap, isTrue); diff --git a/test/swipe_animation_test.dart b/test/swipe_animation_test.dart index a2ba6fd..0b737ce 100644 --- a/test/swipe_animation_test.dart +++ b/test/swipe_animation_test.dart @@ -5,65 +5,68 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('SwipeAnimation Tests', () { testWidgets( - 'renders successfully when enableDrag is false and animationController is null', - (tester) async { - await tester.pumpWidget( - WidgetsApp( - color: const Color(0xFFFFFFFF), - home: SwipeAnimation( - animation: const AlwaysStoppedAnimation(1), - animationController: null, - enableDrag: false, - screenMaximumPercentage: 1, - animationDirection: AxisDirection.up, - onClosing: () {}, - builder: (context) => const Text( - 'Content without drag', - textDirection: TextDirection.ltr, + 'renders successfully when enableDrag is false and animationController is null', + (tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + home: SwipeAnimation( + animation: const AlwaysStoppedAnimation(1), + animationController: null, + enableDrag: false, + screenMaximumPercentage: 1, + animationDirection: AxisDirection.up, + onClosing: () {}, + builder: (context) => const Text( + 'Content without drag', + textDirection: TextDirection.ltr, + ), + ), + pageRouteBuilder: (settings, builder) => PageRouteBuilder( + settings: settings, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), ), ), - pageRouteBuilder: (settings, builder) => PageRouteBuilder( - settings: settings, - pageBuilder: (context, animation, secondaryAnimation) => - builder(context), - ), - ), - ); + ); - expect(find.text('Content without drag'), findsOneWidget); - }); + expect(find.text('Content without drag'), findsOneWidget); + }, + ); testWidgets( - 'renders successfully when enableDrag is true with animationController', - (tester) async { - late AnimationController controller; - - await tester.pumpWidget( - WidgetsApp( - color: const Color(0xFFFFFFFF), - home: StatefulBuilder( - builder: (context, setState) { - return _TestSwipeWidget( - onControllerCreated: (c) => controller = c, - ); - }, - ), - pageRouteBuilder: (settings, builder) => PageRouteBuilder( - settings: settings, - pageBuilder: (context, animation, secondaryAnimation) => - builder(context), + 'renders successfully when enableDrag is true with animationController', + (tester) async { + late AnimationController controller; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + home: StatefulBuilder( + builder: (context, setState) { + return _TestSwipeWidget( + onControllerCreated: (c) => controller = c, + ); + }, + ), + pageRouteBuilder: (settings, builder) => PageRouteBuilder( + settings: settings, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + ), ), - ), - ); + ); - expect(find.text('Content with drag'), findsOneWidget); - expect(controller.value, 1); - }); + expect(find.text('Content with drag'), findsOneWidget); + expect(controller.value, 1); + }, + ); }); group('SwipeGestures Tests', () { - testWidgets('SwipeDownRightGestures handles drag updates and edge cases', - (tester) async { + testWidgets('SwipeDownRightGestures handles drag updates and edge cases', ( + tester, + ) async { var closed = false; late AnimationController controller; @@ -72,10 +75,7 @@ void main() { color: const Color(0xFFFFFFFF), home: _GestureHostWidget( onInit: (vsync) { - controller = AnimationController( - vsync: vsync, - value: 1, - ); + controller = AnimationController(vsync: vsync, value: 1); }, child: const SizedBox(width: 200, height: 200), ), @@ -87,27 +87,28 @@ void main() { ), ); - final gestures = SwipeDownRightGestures( - obtainSize: () => 100, - animationController: controller, - canDragDone: () => false, - onClosing: () => closed = true, - )..handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - delta: const Offset(20, 0), - primaryDelta: 20, - ), - ); + final gestures = + SwipeDownRightGestures( + obtainSize: () => 100, + animationController: controller, + canDragDone: () => false, + onClosing: () => closed = true, + )..handleDragUpdate( + DragUpdateDetails( + globalPosition: Offset.zero, + delta: const Offset(20, 0), + primaryDelta: 20, + ), + ); expect(controller.value, 0.8); // 1.0 - (20 / 100) = 0.8 // Zero size guard (should not produce NaN or change value) SwipeDownRightGestures( - obtainSize: () => 0, - animationController: controller, - canDragDone: () => false, - onClosing: () => closed = true, - ) + obtainSize: () => 0, + animationController: controller, + canDragDone: () => false, + onClosing: () => closed = true, + ) ..handleDragUpdate( DragUpdateDetails( globalPosition: Offset.zero, @@ -116,10 +117,7 @@ void main() { ), ) ..handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - primaryDelta: null, - ), + DragUpdateDetails(globalPosition: Offset.zero, primaryDelta: null), ); expect(controller.value, 0.8); @@ -134,8 +132,9 @@ void main() { controller.dispose(); }); - testWidgets('SwipeUpLeftGestures handles drag updates and edge cases', - (tester) async { + testWidgets('SwipeUpLeftGestures handles drag updates and edge cases', ( + tester, + ) async { var closed = false; late AnimationController controller; @@ -144,10 +143,7 @@ void main() { color: const Color(0xFFFFFFFF), home: _GestureHostWidget( onInit: (vsync) { - controller = AnimationController( - vsync: vsync, - value: 0, - ); + controller = AnimationController(vsync: vsync, value: 0); }, child: const SizedBox(width: 200, height: 200), ), @@ -159,18 +155,19 @@ void main() { ), ); - final gestures = SwipeUpLeftGestures( - obtainSize: () => 100, - animationController: controller, - canDragDone: () => false, - onClosing: () => closed = true, - )..handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - delta: const Offset(30, 0), - primaryDelta: 30, - ), - ); + final gestures = + SwipeUpLeftGestures( + obtainSize: () => 100, + animationController: controller, + canDragDone: () => false, + onClosing: () => closed = true, + )..handleDragUpdate( + DragUpdateDetails( + globalPosition: Offset.zero, + delta: const Offset(30, 0), + primaryDelta: 30, + ), + ); expect(controller.value, 0.3); // 0.0 + (30 / 100) = 0.3 // Drag end below closePercentage forward or fling negative for pop @@ -187,8 +184,7 @@ void main() { group('SwipeChildLayoutDelegate Tests', () { test('Calculates constraints and positions for each AxisDirection', () { - const delegateUp = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.up); + const delegateUp = SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.up); final constraints = delegateUp.getConstraintsForChild( const BoxConstraints(maxWidth: 400, maxHeight: 800), ); @@ -203,8 +199,11 @@ void main() { expect(posUp, const Offset(0, 800 - 400 * 0.5)); // Position down - const delegateDown = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.down); + const delegateDown = SwipeChildLayoutDelegate( + 0.5, + 0.8, + AxisDirection.down, + ); final posDown = delegateDown.getPositionForChild( const Size(400, 800), const Size(400, 400), @@ -212,8 +211,11 @@ void main() { expect(posDown, const Offset(0, 800 - 400)); // Position right - const delegateRight = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.right); + const delegateRight = SwipeChildLayoutDelegate( + 0.5, + 0.8, + AxisDirection.right, + ); final posRight = delegateRight.getPositionForChild( const Size(400, 800), const Size(400, 400), @@ -221,8 +223,11 @@ void main() { expect(posRight, const Offset(400 * 0.5 - 400, 800 - 400)); // Position left - const delegateLeft = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.left); + const delegateLeft = SwipeChildLayoutDelegate( + 0.5, + 0.8, + AxisDirection.left, + ); final posLeft = delegateLeft.getPositionForChild( const Size(400, 800), const Size(400, 400), @@ -230,16 +235,21 @@ void main() { expect(posLeft, const Offset(400 - 400 * 0.5, 800 - 400)); // shouldRelayout - const delegateSame = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.up); + const delegateSame = SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.up); expect(delegateUp.shouldRelayout(delegateSame), isFalse); - const delegateDiffProgress = - SwipeChildLayoutDelegate(0.7, 0.8, AxisDirection.up); + const delegateDiffProgress = SwipeChildLayoutDelegate( + 0.7, + 0.8, + AxisDirection.up, + ); expect(delegateUp.shouldRelayout(delegateDiffProgress), isTrue); - const delegateDiffDirection = - SwipeChildLayoutDelegate(0.5, 0.8, AxisDirection.left); + const delegateDiffDirection = SwipeChildLayoutDelegate( + 0.5, + 0.8, + AxisDirection.left, + ); expect(delegateUp.shouldRelayout(delegateDiffDirection), isTrue); }); }); @@ -284,10 +294,8 @@ class _TestSwipeWidgetState extends State<_TestSwipeWidget> screenMaximumPercentage: 1, animationDirection: AxisDirection.up, onClosing: () {}, - builder: (context) => const Text( - 'Content with drag', - textDirection: TextDirection.ltr, - ), + builder: (context) => + const Text('Content with drag', textDirection: TextDirection.ltr), ); } } diff --git a/test/swipe_direction_and_physics_test.dart b/test/swipe_direction_and_physics_test.dart index aaf5cb9..e1c6ab5 100644 --- a/test/swipe_direction_and_physics_test.dart +++ b/test/swipe_direction_and_physics_test.dart @@ -4,248 +4,250 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('SwipeAnimation All AxisDirections & Notifications', () { - testWidgets('SwipeAnimation builds with AxisDirection.left and AxisDirection.right', ( - tester, - ) async { - final controller = AnimationController( - vsync: const TestVSync(), - duration: const Duration(milliseconds: 300), - ); - - // 1. AxisDirection.left - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: SwipeAnimation( - animation: controller, - animationController: controller, - screenMaximumPercentage: 0.8, - enableDrag: true, - animationDirection: AxisDirection.left, - onClosing: () {}, - builder: (context) => const Text('Left Content'), + testWidgets( + 'SwipeAnimation builds with AxisDirection.left and AxisDirection.right', + (tester) async { + final controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + // 1. AxisDirection.left + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: SwipeAnimation( + animation: controller, + animationController: controller, + screenMaximumPercentage: 0.8, + enableDrag: true, + animationDirection: AxisDirection.left, + onClosing: () {}, + builder: (context) => const Text('Left Content'), + ), ), ), - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Left Content'), findsOneWidget); - - // 2. AxisDirection.right - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: SwipeAnimation( - animation: controller, - animationController: controller, - screenMaximumPercentage: 0.8, - enableDrag: true, - animationDirection: AxisDirection.right, - onClosing: () {}, - builder: (context) => const Text('Right Content'), + ); + await tester.pumpAndSettle(); + expect(find.text('Left Content'), findsOneWidget); + + // 2. AxisDirection.right + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: SwipeAnimation( + animation: controller, + animationController: controller, + screenMaximumPercentage: 0.8, + enableDrag: true, + animationDirection: AxisDirection.right, + onClosing: () {}, + builder: (context) => const Text('Right Content'), + ), ), ), - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Right Content'), findsOneWidget); - - // 3. AxisDirection.down - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: SwipeAnimation( - animation: controller, - animationController: controller, - screenMaximumPercentage: 0.8, - enableDrag: true, - animationDirection: AxisDirection.down, - onClosing: () {}, - builder: (context) => const Text('Down Content'), + ); + await tester.pumpAndSettle(); + expect(find.text('Right Content'), findsOneWidget); + + // 3. AxisDirection.down + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: SwipeAnimation( + animation: controller, + animationController: controller, + screenMaximumPercentage: 0.8, + enableDrag: true, + animationDirection: AxisDirection.down, + onClosing: () {}, + builder: (context) => const Text('Down Content'), + ), ), ), - ), - ); - await tester.pumpAndSettle(); - expect(find.text('Down Content'), findsOneWidget); - }); - - testWidgets('SwipeAnimation notification onClosing when minExtent reached', ( - tester, - ) async { - final controller = AnimationController( - vsync: const TestVSync(), - duration: const Duration(milliseconds: 300), - ); - - var closingFired = false; - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: SwipeAnimation( - animation: controller, - animationController: controller, - screenMaximumPercentage: 1, - enableDrag: true, - animationDirection: AxisDirection.up, - onClosing: () { - closingFired = true; - }, - builder: (context) => const Text('Dispatch Target'), + ); + await tester.pumpAndSettle(); + expect(find.text('Down Content'), findsOneWidget); + }, + ); + + testWidgets( + 'SwipeAnimation notification onClosing when minExtent reached', + (tester) async { + final controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + var closingFired = false; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: SwipeAnimation( + animation: controller, + animationController: controller, + screenMaximumPercentage: 1, + enableDrag: true, + animationDirection: AxisDirection.up, + onClosing: () { + closingFired = true; + }, + builder: (context) => const Text('Dispatch Target'), + ), ), ), - ), - ); - await tester.pumpAndSettle(); - - final childContext = tester.element(find.text('Dispatch Target')); - DraggableScrollableNotification( - extent: 0.2, - minExtent: 0.2, - maxExtent: 1, - initialExtent: 0.5, - context: childContext, - ).dispatch(childContext); - await tester.pumpAndSettle(); - - expect(closingFired, isTrue); - }); + ); + await tester.pumpAndSettle(); + + final childContext = tester.element(find.text('Dispatch Target')); + DraggableScrollableNotification( + extent: 0.2, + minExtent: 0.2, + maxExtent: 1, + initialExtent: 0.5, + context: childContext, + ).dispatch(childContext); + await tester.pumpAndSettle(); + + expect(closingFired, isTrue); + }, + ); }); group('SwipeGestures Drag Updates and Fling Physics', () { - test('SwipeDownRightGestures handles start, update with zero size, and end', () { - final controller = AnimationController( - vsync: const TestVSync(), - duration: const Duration(milliseconds: 300), - ); - - var startFired = false; - var endFired = false; - var closingFired = false; - - final gestures = SwipeDownRightGestures( - animationController: controller, - obtainSize: () => 100, - canDragDone: () => false, - onClosing: () { - closingFired = true; - }, - dragStart: (d) => startFired = true, - dragEnd: (d) => endFired = true, - )..handleDragStart(DragStartDetails()); - expect(startFired, isTrue); - - // 2. handleDragUpdate with normal delta - controller.value = 0.8; - gestures.handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - delta: const Offset(0, 10), - primaryDelta: 10, - ), - ); - expect(controller.value, lessThan(0.8)); - - // 3. handleDragUpdate with zero size - SwipeDownRightGestures( - animationController: controller, - obtainSize: () => 0, - canDragDone: () => false, - onClosing: () {}, - ).handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - delta: const Offset(0, 5), - primaryDelta: 5, - ), - ); - - // 4. handleDragEnd with fast positive velocity (fling right/down) - gestures.handleDragEnd( - DragEndDetails( - velocity: const Velocity(pixelsPerSecond: Offset(0, 200)), - primaryVelocity: 200, - ), - ); - expect(closingFired, isTrue); - expect(endFired, isTrue); - - // 5. canDragDone true prevents updates - const canDone = true; - SwipeDownRightGestures( - animationController: controller, - obtainSize: () => 100, - canDragDone: () => canDone, - onClosing: () {}, - ) - ..handleDragUpdate( + test( + 'SwipeDownRightGestures handles start, update with zero size, and end', + () { + final controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + var startFired = false; + var endFired = false; + var closingFired = false; + + final gestures = SwipeDownRightGestures( + animationController: controller, + obtainSize: () => 100, + canDragDone: () => false, + onClosing: () { + closingFired = true; + }, + dragStart: (d) => startFired = true, + dragEnd: (d) => endFired = true, + )..handleDragStart(DragStartDetails()); + expect(startFired, isTrue); + + // 2. handleDragUpdate with normal delta + controller.value = 0.8; + gestures.handleDragUpdate( DragUpdateDetails( globalPosition: Offset.zero, delta: const Offset(0, 10), primaryDelta: 10, ), - ) - ..handleDragEnd(DragEndDetails()); - }); - - test('SwipeUpLeftGestures handles update and slow drag below/above threshold', () { - final controller = AnimationController( - vsync: const TestVSync(), - duration: const Duration(milliseconds: 300), - ); - - var closingFired = false; - - final gestures = SwipeUpLeftGestures( - animationController: controller, - obtainSize: () => 100, - canDragDone: () => false, - onClosing: () { - closingFired = true; - }, - closePercentage: 0.5, - ); - - // 1. handleDragUpdate - controller.value = 0.2; - gestures.handleDragUpdate( - DragUpdateDetails( - globalPosition: Offset.zero, - delta: const Offset(0, 10), - primaryDelta: 10, - ), - ); - expect(controller.value, closeTo(0.3, 0.001)); - - // 2. handleDragEnd below closePercentage -> pops - controller.value = 0.4; - gestures.handleDragEnd( - DragEndDetails( - velocity: Velocity.zero, - primaryVelocity: 0, - ), - ); - expect(closingFired, isTrue); - - // 3. handleDragEnd above closePercentage -> forward - closingFired = false; - controller.value = 0.8; - gestures.handleDragEnd( - DragEndDetails( - velocity: Velocity.zero, - primaryVelocity: 0, - ), - ); - expect(closingFired, isFalse); - }); + ); + expect(controller.value, lessThan(0.8)); + + // 3. handleDragUpdate with zero size + SwipeDownRightGestures( + animationController: controller, + obtainSize: () => 0, + canDragDone: () => false, + onClosing: () {}, + ).handleDragUpdate( + DragUpdateDetails( + globalPosition: Offset.zero, + delta: const Offset(0, 5), + primaryDelta: 5, + ), + ); + + // 4. handleDragEnd with fast positive velocity (fling right/down) + gestures.handleDragEnd( + DragEndDetails( + velocity: const Velocity(pixelsPerSecond: Offset(0, 200)), + primaryVelocity: 200, + ), + ); + expect(closingFired, isTrue); + expect(endFired, isTrue); + + // 5. canDragDone true prevents updates + const canDone = true; + SwipeDownRightGestures( + animationController: controller, + obtainSize: () => 100, + canDragDone: () => canDone, + onClosing: () {}, + ) + ..handleDragUpdate( + DragUpdateDetails( + globalPosition: Offset.zero, + delta: const Offset(0, 10), + primaryDelta: 10, + ), + ) + ..handleDragEnd(DragEndDetails()); + }, + ); + + test( + 'SwipeUpLeftGestures handles update and slow drag below/above threshold', + () { + final controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + var closingFired = false; + + final gestures = SwipeUpLeftGestures( + animationController: controller, + obtainSize: () => 100, + canDragDone: () => false, + onClosing: () { + closingFired = true; + }, + closePercentage: 0.5, + ); + + // 1. handleDragUpdate + controller.value = 0.2; + gestures.handleDragUpdate( + DragUpdateDetails( + globalPosition: Offset.zero, + delta: const Offset(0, 10), + primaryDelta: 10, + ), + ); + expect(controller.value, closeTo(0.3, 0.001)); + + // 2. handleDragEnd below closePercentage -> pops + controller.value = 0.4; + gestures.handleDragEnd( + DragEndDetails(velocity: Velocity.zero, primaryVelocity: 0), + ); + expect(closingFired, isTrue); + + // 3. handleDragEnd above closePercentage -> forward + closingFired = false; + controller.value = 0.8; + gestures.handleDragEnd( + DragEndDetails(velocity: Velocity.zero, primaryVelocity: 0), + ); + expect(closingFired, isFalse); + }, + ); }); }