| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../widgets/semantics_tester.dart'; |
| |
| void main() { |
| // Pumps and ensures that the BottomSheet animates non-linearly. |
| Future<void> checkNonLinearAnimation(WidgetTester tester) async { |
| final Offset firstPosition = tester.getCenter(find.text('BottomSheet')); |
| await tester.pump(const Duration(milliseconds: 30)); |
| final Offset secondPosition = tester.getCenter(find.text('BottomSheet')); |
| await tester.pump(const Duration(milliseconds: 30)); |
| final Offset thirdPosition = tester.getCenter(find.text('BottomSheet')); |
| |
| final double dyDelta1 = secondPosition.dy - firstPosition.dy; |
| final double dyDelta2 = thirdPosition.dy - secondPosition.dy; |
| |
| // If the animation were linear, these two values would be the same. |
| expect(dyDelta1, isNot(moreOrLessEquals(dyDelta2, epsilon: 0.1))); |
| } |
| |
| testWidgets('Throw if enable drag without an animation controller', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/89168 |
| await tester.pumpWidget( |
| MaterialApp( |
| home: BottomSheet( |
| onClosing: () {}, |
| builder: (_) => Container( |
| height: 200, |
| color: Colors.red, |
| child: const Text('BottomSheet'), |
| ), |
| ), |
| ), |
| ); |
| |
| final FlutterExceptionHandler? handler = FlutterError.onError; |
| FlutterErrorDetails? error; |
| FlutterError.onError = (FlutterErrorDetails details) { |
| error = details; |
| }; |
| |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| |
| expect(error, isNotNull); |
| FlutterError.onError = handler; |
| }); |
| |
| testWidgets('Disposing app while bottom sheet is disappearing does not crash', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| // Bring up bottom sheet. |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Start closing animation of Bottom sheet. |
| tester.state<NavigatorState>(find.byType(Navigator)).pop(); |
| await tester.pump(); |
| |
| // Dispose app by replacing it with a container. This shouldn't crash. |
| await tester.pumpWidget(Container()); |
| }); |
| |
| |
| testWidgets('Swiping down a BottomSheet should dismiss it by default', (WidgetTester tester) async { |
| |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| bool showBottomSheetThenCalled = false; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) { |
| return const SizedBox( |
| height: 200.0, |
| child: Text('BottomSheet'), |
| ); |
| }).closed.whenComplete(() { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Swiping down a BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { |
| |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| bool showBottomSheetThenCalled = false; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) { |
| return const SizedBox( |
| height: 200.0, |
| child: Text('BottomSheet'), |
| ); |
| }, |
| enableDrag: false |
| ).closed.whenComplete(() { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Swipe the bottom sheet, attempting to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet should not dismiss. |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| }); |
| |
| testWidgets('Swiping down a BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| bool showBottomSheetThenCalled = false; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) { |
| return const SizedBox( |
| height: 200.0, |
| child: Text('BottomSheet'), |
| ); |
| }, |
| enableDrag: true |
| ).closed.whenComplete(() { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| int numBuilderCalls = 0; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| isDismissible: false, |
| builder: (BuildContext context) { |
| numBuilderCalls++; |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pumpAndSettle(); |
| expect(numBuilderCalls, 1); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(numBuilderCalls, 1); |
| }); |
| |
| testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Tap on the bottom sheet itself, it should not be dismissed |
| await tester.tap(find.text('BottomSheet')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| }); |
| |
| testWidgets('Tapping outside a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Tap above the bottom sheet to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Tapping outside a modal BottomSheet should dismiss it when isDismissible=true', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Tap above the bottom sheet to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Verify that the BottomSheet animates non-linearly', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| await tester.pump(); |
| |
| await checkNonLinearAnimation(tester); |
| await tester.pumpAndSettle(); |
| |
| // Tap above the bottom sheet to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); |
| await tester.pump(); |
| await checkNonLinearAnimation(tester); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| isDismissible: false, |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Tap above the bottom sheet, attempting to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); |
| await tester.pumpAndSettle(); // Bottom sheet should not dismiss. |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| }); |
| |
| testWidgets('Swiping down a modal BottomSheet should dismiss it by default', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| isDismissible: false, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Swiping down a modal BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| isDismissible: false, |
| enableDrag: false, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Swipe the bottom sheet, attempting to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet should not dismiss. |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| }); |
| |
| testWidgets('Swiping down a modal BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| bool showBottomSheetThenCalled = false; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| isDismissible: false, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ).then<void>((void value) { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect(showBottomSheetThenCalled, isFalse); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Modal BottomSheet builder should only be called once', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| )); |
| |
| int numBuilderCalls = 0; |
| showModalBottomSheet<void>( |
| context: savedContext, |
| isDismissible: false, |
| builder: (BuildContext context) { |
| numBuilderCalls++; |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pumpAndSettle(); |
| expect(numBuilderCalls, 1); |
| |
| // Swipe the bottom sheet to dismiss it. |
| await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0)); |
| await tester.pumpAndSettle(); // Bottom sheet dismiss animation. |
| expect(numBuilderCalls, 1); |
| }); |
| |
| testWidgets('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| bool showBottomSheetThenCalled = false; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) { |
| return Container( |
| margin: const EdgeInsets.all(40.0), |
| child: const Text('BottomSheet'), |
| ); |
| }).closed.whenComplete(() { |
| showBottomSheetThenCalled = true; |
| }); |
| |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.pump(); // bottom sheet show animation starts |
| |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| expect(showBottomSheetThenCalled, isFalse); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // The fling below must be such that the velocity estimation examines an |
| // offset greater than the kTouchSlop. Too slow or too short a distance, and |
| // it won't trigger. Also, it must not be so much that it drags the bottom |
| // sheet off the screen, or we won't see it after we pump! |
| await tester.fling(find.text('BottomSheet'), const Offset(0.0, 50.0), 2000.0); |
| await tester.pump(); // drain the microtask queue (Future completion callback) |
| |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(); // bottom sheet dismiss animation starts |
| |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| expect(showBottomSheetThenCalled, isTrue); |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('Verify that dragging past the bottom dismisses a persistent BottomSheet', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/5528 |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) { |
| return Container( |
| margin: const EdgeInsets.all(40.0), |
| child: const Text('BottomSheet'), |
| ); |
| }); |
| |
| await tester.pump(); // bottom sheet show animation starts |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.fling(find.text('BottomSheet'), const Offset(0.0, 400.0), 1000.0); |
| await tester.pump(); // drain the microtask queue (Future completion callback) |
| await tester.pump(); // bottom sheet dismiss animation starts |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| testWidgets('modal BottomSheet has no top MediaQuery', (WidgetTester tester) async { |
| late BuildContext outerContext; |
| late BuildContext innerContext; |
| |
| await tester.pumpWidget(Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: const <LocalizationsDelegate<dynamic>>[ |
| DefaultWidgetsLocalizations.delegate, |
| DefaultMaterialLocalizations.delegate, |
| ], |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData( |
| padding: EdgeInsets.all(50.0), |
| size: Size(400.0, 600.0), |
| ), |
| child: Navigator( |
| onGenerateRoute: (_) { |
| return PageRouteBuilder<void>( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| outerContext = context; |
| return Container(); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ), |
| )); |
| |
| showModalBottomSheet<void>( |
| context: outerContext, |
| builder: (BuildContext context) { |
| innerContext = context; |
| return Container(); |
| }, |
| ); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect( |
| MediaQuery.of(outerContext).padding, |
| const EdgeInsets.all(50.0), |
| ); |
| expect( |
| MediaQuery.of(innerContext).padding, |
| const EdgeInsets.only(left: 50.0, right: 50.0, bottom: 50.0), |
| ); |
| }); |
| |
| testWidgets('modal BottomSheet can insert a SafeArea', (WidgetTester tester) async { |
| late BuildContext outerContext; |
| late BuildContext innerContext; |
| |
| await tester.pumpWidget(Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: const <LocalizationsDelegate<dynamic>>[ |
| DefaultWidgetsLocalizations.delegate, |
| DefaultMaterialLocalizations.delegate, |
| ], |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData( |
| padding: EdgeInsets.all(50.0), |
| size: Size(400.0, 600.0), |
| ), |
| child: Navigator( |
| onGenerateRoute: (_) { |
| return PageRouteBuilder<void>( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| outerContext = context; |
| return Container(); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ), |
| )); |
| |
| // Without a SafeArea (useSafeArea is false by default) |
| showModalBottomSheet<void>( |
| context: outerContext, |
| builder: (BuildContext context) { |
| innerContext = context; |
| return Container(); |
| }, |
| ); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Top padding is consumed and there is no SafeArea |
| expect(MediaQuery.of(innerContext).padding.top, 0); |
| expect(find.byType(SafeArea), findsNothing); |
| |
| // With a SafeArea |
| showModalBottomSheet<void>( |
| context: outerContext, |
| useSafeArea: true, |
| builder: (BuildContext context) { |
| innerContext = context; |
| return Container(); |
| }, |
| ); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Top padding is consumed and there is a SafeArea |
| expect(MediaQuery.of(innerContext).padding.top, 0); |
| expect(find.byType(SafeArea), findsOneWidget); |
| }); |
| |
| testWidgets('modal BottomSheet has semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| |
| showModalBottomSheet<void>(context: scaffoldKey.currentContext!, builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }); |
| |
| await tester.pump(); // bottom sheet show animation starts |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| children: <TestSemantics>[ |
| TestSemantics( |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'Dialog', |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| SemanticsFlag.namesRoute, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'BottomSheet', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| TestSemantics(), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| const Color color = Colors.pink; |
| const double elevation = 9.0; |
| const ShapeBorder shape = BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))); |
| const Clip clipBehavior = Clip.antiAlias; |
| const Color barrierColor = Colors.red; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| backgroundColor: color, |
| barrierColor: barrierColor, |
| elevation: elevation, |
| shape: shape, |
| clipBehavior: clipBehavior, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| final BottomSheet bottomSheet = tester.widget(find.byType(BottomSheet)); |
| expect(bottomSheet.backgroundColor, color); |
| expect(bottomSheet.elevation, elevation); |
| expect(bottomSheet.shape, shape); |
| expect(bottomSheet.clipBehavior, clipBehavior); |
| |
| final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); |
| expect(modalBarrier.color, barrierColor); |
| }); |
| |
| testWidgets('modal BottomSheet with scrollController has semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| builder: (BuildContext context) { |
| return DraggableScrollableSheet( |
| expand: false, |
| builder: (_, ScrollController controller) { |
| return SingleChildScrollView( |
| controller: controller, |
| child: const Text('BottomSheet'), |
| ); |
| }, |
| ); |
| }, |
| ); |
| |
| await tester.pump(); // bottom sheet show animation starts |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| children: <TestSemantics>[ |
| TestSemantics( |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'Dialog', |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| SemanticsFlag.namesRoute, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'BottomSheet', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| TestSemantics(), |
| ], |
| ), |
| ], |
| ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) { |
| return const _TestPage(); |
| })), |
| bottomNavigationBar: BottomNavigationBar( |
| items: const <BottomNavigationBarItem>[ |
| BottomNavigationBarItem( |
| icon: Icon(Icons.ac_unit), |
| label: 'Item 1', |
| ), |
| BottomNavigationBarItem( |
| icon: Icon(Icons.style), |
| label: 'Item 2', |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('Show bottom sheet')); |
| await tester.pumpAndSettle(); |
| |
| // Bottom sheet is displayed in correct position within the inner navigator |
| // and above the BottomNavigationBar. |
| expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 544.0); |
| }); |
| |
| testWidgets('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) { |
| return const _TestPage(useRootNavigator: true); |
| })), |
| bottomNavigationBar: BottomNavigationBar( |
| items: const <BottomNavigationBarItem>[ |
| BottomNavigationBarItem( |
| icon: Icon(Icons.ac_unit), |
| label: 'Item 1', |
| ), |
| BottomNavigationBarItem( |
| icon: Icon(Icons.style), |
| label: 'Item 2', |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('Show bottom sheet')); |
| await tester.pumpAndSettle(); |
| |
| // Bottom sheet is displayed in correct position above all content including |
| // the BottomNavigationBar. |
| expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600.0); |
| }); |
| |
| testWidgets('Verify that route settings can be set in the showModalBottomSheet', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| const RouteSettings routeSettings = RouteSettings(name: 'route_name', arguments: 'route_argument'); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| late RouteSettings retrievedRouteSettings; |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| routeSettings: routeSettings, |
| builder: (BuildContext context) { |
| retrievedRouteSettings = ModalRoute.of(context)!.settings; |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(retrievedRouteSettings, routeSettings); |
| }); |
| |
| testWidgets('Verify showModalBottomSheet use AnimationController if provided.', (WidgetTester tester) async { |
| const Key tapTarget = Key('tap-target'); |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| key: tapTarget, |
| onTap: () { |
| showModalBottomSheet<void>( |
| context: context, |
| // The default duration and reverseDuration is 1 second |
| transitionAnimationController: AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ), |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| }, |
| behavior: HitTestBehavior.opaque, |
| child: const SizedBox( |
| height: 100.0, |
| width: 100.0, |
| ), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.tap(find.byKey(tapTarget)); // Opening animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Tapping above the bottom sheet to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); // Closing animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| // The bottom sheet should still be present at the very end of the animation. |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| // The bottom sheet should not be showing any longer. |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/87592 |
| testWidgets('the framework do not dispose the transitionAnimationController provided by user.', (WidgetTester tester) async { |
| const Key tapTarget = Key('tap-target'); |
| final AnimationController controller = AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| key: tapTarget, |
| onTap: () { |
| showModalBottomSheet<void>( |
| context: context, |
| // The default duration and reverseDuration is 1 second |
| transitionAnimationController: controller, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| }, |
| behavior: HitTestBehavior.opaque, |
| child: const SizedBox( |
| height: 100.0, |
| width: 100.0, |
| ), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.tap(find.byKey(tapTarget)); // Opening animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Tapping above the bottom sheet to dismiss it. |
| await tester.tapAt(const Offset(20.0, 20.0)); // Closing animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| // The bottom sheet should still be present at the very end of the animation. |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| // The bottom sheet should not be showing any longer. |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| controller.dispose(); |
| // Double disposal will throw. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Verify persistence BottomSheet use AnimationController if provided.', (WidgetTester tester) async { |
| const Key tapTarget = Key('tap-target'); |
| const Key tapTargetToClose = Key('tap-target-to-close'); |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| key: tapTarget, |
| onTap: () { |
| showBottomSheet<void>( |
| context: context, |
| // The default duration and reverseDuration is 1 second |
| transitionAnimationController: AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ), |
| builder: (BuildContext context) { |
| return MaterialButton( |
| onPressed: () => Navigator.pop(context), |
| key: tapTargetToClose, |
| child: const Text('BottomSheet'), |
| ); |
| }, |
| ); |
| }, |
| behavior: HitTestBehavior.opaque, |
| child: const SizedBox( |
| height: 100.0, |
| width: 100.0, |
| ), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.tap(find.byKey(tapTarget)); // Opening animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Tapping button on the bottom sheet to dismiss it. |
| await tester.tap(find.byKey(tapTargetToClose)); // Closing animation will start after tapping |
| await tester.pump(); |
| |
| expect(find.text('BottomSheet'), findsOneWidget); |
| await tester.pump(const Duration(milliseconds: 2000)); |
| // The bottom sheet should still be present at the very end of the animation. |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| // The bottom sheet should not be showing any longer. |
| expect(find.text('BottomSheet'), findsNothing); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/87708 |
| testWidgets('Each of the internal animation controllers should be disposed by the framework.', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((_) { |
| return Builder( |
| builder: (BuildContext context) { |
| return Container(height: 200.0); |
| }, |
| ); |
| }); |
| |
| await tester.pump(); |
| expect(find.byType(BottomSheet), findsOneWidget); |
| |
| // The first sheet's animation is still running. |
| |
| // Trigger the second sheet will remove the first sheet from tree. |
| scaffoldKey.currentState!.showBottomSheet<void>((_) { |
| return Builder( |
| builder: (BuildContext context) { |
| return Container(height: 200.0); |
| }, |
| ); |
| }); |
| await tester.pump(); |
| expect(find.byType(BottomSheet), findsOneWidget); |
| |
| // Remove the Scaffold from the tree. |
| await tester.pumpWidget(const SizedBox.shrink()); |
| |
| // If the internal animation controller do not dispose will throw |
| // FlutterError:<ScaffoldState#1981a(tickers: tracking 1 ticker) was disposed with an active |
| // Ticker. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/99627 |
| testWidgets('The old route entry should be removed when a new sheet popup', (WidgetTester tester) async { |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); |
| PersistentBottomSheetController<void>? sheetController; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| )); |
| |
| final ModalRoute<dynamic> route = ModalRoute.of(scaffoldKey.currentContext!)!; |
| expect(route.canPop, false); |
| |
| scaffoldKey.currentState!.showBottomSheet<void>((_) { |
| return Builder( |
| builder: (BuildContext context) { |
| return Container(height: 200.0); |
| }, |
| ); |
| }); |
| |
| await tester.pump(); |
| expect(find.byType(BottomSheet), findsOneWidget); |
| expect(route.canPop, true); |
| |
| // Trigger the second sheet will remove the first sheet from tree. |
| sheetController = scaffoldKey.currentState!.showBottomSheet<void>((_) { |
| return Builder( |
| builder: (BuildContext context) { |
| return Container(height: 200.0); |
| }, |
| ); |
| }); |
| await tester.pump(); |
| expect(find.byType(BottomSheet), findsOneWidget); |
| expect(route.canPop, true); |
| |
| sheetController.close(); |
| |
| expect(route.canPop, false); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/87708 |
| testWidgets('The framework does not dispose of the transitionAnimationController provided by user.', (WidgetTester tester) async { |
| const Key tapTarget = Key('tap-target'); |
| const Key tapTargetToClose = Key('tap-target-to-close'); |
| final AnimationController controller = AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ); |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| key: tapTarget, |
| onTap: () { |
| showBottomSheet<void>( |
| context: context, |
| transitionAnimationController: controller, |
| builder: (BuildContext context) { |
| return MaterialButton( |
| onPressed: () => Navigator.pop(context), |
| key: tapTargetToClose, |
| child: const Text('BottomSheet'), |
| ); |
| }, |
| ); |
| }, |
| behavior: HitTestBehavior.opaque, |
| child: const SizedBox( |
| height: 100.0, |
| width: 100.0, |
| ), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.tap(find.byKey(tapTarget)); // Open the sheet. |
| await tester.pumpAndSettle(); // Finish the animation. |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Tapping button on the bottom sheet to dismiss it. |
| await tester.tap(find.byKey(tapTargetToClose)); // Closing the sheet. |
| await tester.pumpAndSettle(); // Finish the animation. |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| await tester.pumpWidget(const SizedBox.shrink()); |
| controller.dispose(); |
| |
| // Double dispose will throw. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Calling PersistentBottomSheetController.close does not crash when it is not the current bottom sheet', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/93717 |
| PersistentBottomSheetController<void>? sheetController1; |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return SafeArea( |
| child: Column( |
| children: <Widget>[ |
| ElevatedButton( |
| child: const Text('show 1'), |
| onPressed: () { |
| sheetController1 = Scaffold.of(context).showBottomSheet<void>( |
| (BuildContext context) => const Text('BottomSheet 1'), |
| ); |
| }, |
| ), |
| ElevatedButton( |
| child: const Text('show 2'), |
| onPressed: () { |
| Scaffold.of(context).showBottomSheet<void>( |
| (BuildContext context) => const Text('BottomSheet 2'), |
| ); |
| }, |
| ), |
| ElevatedButton( |
| child: const Text('close 1'), |
| onPressed: (){ |
| sheetController1!.close(); |
| }, |
| ), |
| ], |
| ), |
| ); |
| }), |
| ), |
| )); |
| |
| await tester.tap(find.text('show 1')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet 1'), findsOneWidget); |
| |
| await tester.tap(find.text('show 2')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet 2'), findsOneWidget); |
| |
| // This will throw an assertion if regressed |
| await tester.tap(find.text('close 1')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet 2'), findsOneWidget); |
| }); |
| |
| group('Modal BottomSheet avoids overlapping display features', () { |
| testWidgets('positioning using anchorPoint', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: child!, |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) { |
| return const Placeholder(); |
| }, |
| anchorPoint: const Offset(1000, 0), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Should take the right side of the screen |
| expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410); |
| expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); |
| }); |
| |
| testWidgets('positioning using Directionality', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: Directionality( |
| textDirection: TextDirection.rtl, |
| child: child!, |
| ), |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) { |
| return const Placeholder(); |
| }, |
| ); |
| await tester.pumpAndSettle(); |
| |
| // This is RTL, so it should place the dialog on the right screen |
| expect(tester.getTopLeft(find.byType(Placeholder)).dx, 410); |
| expect(tester.getBottomRight(find.byType(Placeholder)).dx, 800); |
| }); |
| |
| testWidgets('default positioning', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: child!, |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) { |
| return const Placeholder(); |
| }, |
| ); |
| await tester.pumpAndSettle(); |
| |
| // By default it should place the dialog on the left screen |
| expect(tester.getTopLeft(find.byType(Placeholder)).dx, 0.0); |
| expect(tester.getBottomRight(find.byType(Placeholder)).dx, 390.0); |
| }); |
| }); |
| |
| group('constraints', () { |
| |
| testWidgets('No constraints by default for bottomSheet property', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp( |
| home: Scaffold( |
| body: Center(child: Text('body')), |
| bottomSheet: Text('BottomSheet'), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(0, 586, 154, 600), |
| ); |
| }); |
| |
| testWidgets('No constraints by default for showBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| Scaffold.of(context).showBottomSheet<void>( |
| (BuildContext context) => const Text('BottomSheet'), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(0, 586, 154, 600), |
| ); |
| }); |
| |
| testWidgets('No constraints by default for showModalBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(0, 586, 800, 600), |
| ); |
| }); |
| |
| testWidgets('Theme constraints used for bottomSheet property', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: 80), |
| ), |
| ), |
| home: Scaffold( |
| body: const Center(child: Text('body')), |
| bottomSheet: const Text('BottomSheet'), |
| floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| // Should be centered and only 80dp wide |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(360, 558, 440, 600), |
| ); |
| // Ensure the FAB is overlapping the top of the sheet |
| expect(find.byIcon(Icons.add), findsOneWidget); |
| expect( |
| tester.getRect(find.byIcon(Icons.add)), |
| const Rect.fromLTRB(744, 544, 768, 568), |
| ); |
| }); |
| |
| testWidgets('Theme constraints used for showBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: 80), |
| ), |
| ), |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| Scaffold.of(context).showBottomSheet<void>( |
| (BuildContext context) => const Text('BottomSheet'), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| // Should be centered and only 80dp wide |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(360, 558, 440, 600), |
| ); |
| }); |
| |
| testWidgets('Theme constraints used for showModalBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: 80), |
| ), |
| ), |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| // Should be centered and only 80dp wide |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(360, 558, 440, 600), |
| ); |
| }); |
| |
| testWidgets('constraints param overrides theme for showBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: 80), |
| ), |
| ), |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| Scaffold.of(context).showBottomSheet<void>( |
| (BuildContext context) => const Text('BottomSheet'), |
| constraints: const BoxConstraints(maxWidth: 100), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| // Should be centered and only 100dp wide instead of 80dp wide |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(350, 572, 450, 600), |
| ); |
| }); |
| |
| testWidgets('constraints param overrides theme for showModalBottomSheet', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: 80), |
| ), |
| ), |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| constraints: const BoxConstraints(maxWidth: 100), |
| ); |
| }, |
| ), |
| ); |
| }), |
| ), |
| )); |
| expect(find.text('BottomSheet'), findsNothing); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| // Should be centered and only 100dp instead of 80dp wide |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| const Rect.fromLTRB(350, 572, 450, 600), |
| ); |
| }); |
| |
| }); |
| } |
| |
| class _TestPage extends StatelessWidget { |
| const _TestPage({this.useRootNavigator}); |
| |
| final bool? useRootNavigator; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Center( |
| child: TextButton( |
| child: const Text('Show bottom sheet'), |
| onPressed: () { |
| if (useRootNavigator != null) { |
| showModalBottomSheet<void>( |
| useRootNavigator: useRootNavigator!, |
| context: context, |
| builder: (_) => const Text('Modal bottom sheet'), |
| ); |
| } else { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (_) => const Text('Modal bottom sheet'), |
| ); |
| } |
| }, |
| ), |
| ); |
| } |
| } |