| // 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/semantics.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. |
| var 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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| var 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((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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| var 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((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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| var 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((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('Tapping on a BottomSheet should not trigger a rebuild when enableDrag is true', ( |
| WidgetTester tester, |
| ) async { |
| // Regression test for https://github.com/flutter/flutter/issues/126833. |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| var buildCount = 0; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(buildCount, 0); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| scaffoldKey.currentState!.showBottomSheet((BuildContext context) { |
| buildCount++; |
| return const SizedBox(height: 200.0, child: Text('BottomSheet')); |
| }, enableDrag: true); |
| |
| await tester.pumpAndSettle(); |
| expect(buildCount, 1); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| |
| // Tap on bottom sheet should not trigger a rebuild. |
| await tester.tap(find.text('BottomSheet')); |
| await tester.pumpAndSettle(); |
| expect(buildCount, 1); |
| expect(find.text('BottomSheet'), findsOneWidget); |
| }); |
| |
| 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(); |
| }, |
| ), |
| ), |
| ); |
| |
| var 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); |
| |
| var 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); |
| |
| var 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); |
| |
| var 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); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/121098 |
| testWidgets('Verify that accessibleNavigation has no impact on the BottomSheet animation', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery(data: const MediaQueryData(accessibleNavigation: true), child: child!); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('BottomSheet'), findsNothing); |
| |
| final BuildContext homeContext = tester.element(find.text('Test')); |
| showModalBottomSheet<void>( |
| context: homeContext, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| await tester.pump(); |
| |
| await checkNonLinearAnimation(tester); |
| await tester.pumpAndSettle(); |
| }); |
| |
| 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); |
| |
| var 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); |
| |
| var 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); |
| |
| var 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); |
| |
| var 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(); |
| }, |
| ), |
| ), |
| ); |
| |
| var 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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| var 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((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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ), |
| ); |
| |
| scaffoldKey.currentState!.showBottomSheet((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)); |
| |
| // A SafeArea is inserted, with left / top / right true but bottom false. |
| final Finder safeAreaWidgetFinder = find.byType(SafeArea); |
| expect(safeAreaWidgetFinder, findsOneWidget); |
| final safeAreaWidget = safeAreaWidgetFinder.evaluate().single.widget as SafeArea; |
| expect(safeAreaWidget.left, true); |
| expect(safeAreaWidget.top, true); |
| expect(safeAreaWidget.right, true); |
| expect(safeAreaWidget.bottom, false); |
| |
| // Because that SafeArea is inserted, no left / top / right padding remains |
| // for `builder` to consume. Bottom padding does remain. |
| expect(MediaQuery.of(innerContext).padding, const EdgeInsets.fromLTRB(0, 0, 0, 50.0)); |
| }); |
| |
| testWidgets('modal BottomSheet has semantics', (WidgetTester tester) async { |
| final semantics = SemanticsTester(tester); |
| final 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( |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'Dialog', |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| SemanticsFlag.namesRoute, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| TestSemantics( |
| children: <TestSemantics>[ |
| TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], |
| label: 'Scrim', |
| hintOverrides: const SemanticsHintOverrides(onTapHint: 'Close Bottom Sheet'), |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreTransform: true, |
| ignoreRect: true, |
| ignoreId: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async { |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| const Color color = Colors.pink; |
| const 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('Material3 - BottomSheet uses fallback values', (WidgetTester tester) async { |
| const Color surfaceColor = Colors.pink; |
| const Color surfaceTintColor = Colors.blue; |
| const ShapeBorder defaultShape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)), |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| colorScheme: const ColorScheme.light( |
| surface: surfaceColor, |
| surfaceTint: surfaceTintColor, |
| ), |
| ), |
| home: Scaffold( |
| body: BottomSheet( |
| onClosing: () {}, |
| builder: (BuildContext context) { |
| return Container(); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| final Finder finder = find.descendant( |
| of: find.byType(BottomSheet), |
| matching: find.byType(Material), |
| ); |
| final Material material = tester.widget<Material>(finder); |
| |
| expect(material.color, surfaceColor); |
| // Surface tint is no longer used by default. |
| expect(material.surfaceTintColor, Colors.transparent); |
| expect(material.elevation, 1.0); |
| expect(material.shape, defaultShape); |
| expect(tester.getSize(finder).width, 640); |
| }); |
| |
| testWidgets('Material3 - BottomSheet has transparent shadow', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: BottomSheet( |
| onClosing: () {}, |
| builder: (BuildContext context) { |
| return Container(); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| final Material material = tester.widget<Material>( |
| find.descendant(of: find.byType(BottomSheet), matching: find.byType(Material)), |
| ); |
| expect(material.shadowColor, Colors.transparent); |
| }); |
| |
| testWidgets('Material2 - Modal BottomSheet with ScrollController has semantics', ( |
| WidgetTester tester, |
| ) async { |
| final semantics = SemanticsTester(tester); |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| 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( |
| 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( |
| children: <TestSemantics>[ |
| TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], |
| label: 'Scrim', |
| hintOverrides: const SemanticsHintOverrides(onTapHint: 'Close Bottom Sheet'), |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreTransform: true, |
| ignoreRect: true, |
| ignoreId: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Material3 - Modal BottomSheet with ScrollController has semantics', ( |
| WidgetTester tester, |
| ) async { |
| final semantics = SemanticsTester(tester); |
| final 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( |
| 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( |
| children: <TestSemantics>[ |
| TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], |
| label: 'Scrim', |
| hintOverrides: const SemanticsHintOverrides(onTapHint: 'Close Bottom Sheet'), |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreTransform: true, |
| ignoreRect: true, |
| ignoreId: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Material3 - Modal BottomSheet with drag handle has semantics', ( |
| WidgetTester tester, |
| ) async { |
| final semantics = SemanticsTester(tester); |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ), |
| ); |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| showDragHandle: true, |
| 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( |
| children: <TestSemantics>[ |
| TestSemantics( |
| label: 'Dialog', |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| SemanticsFlag.namesRoute, |
| ], |
| children: <TestSemantics>[ |
| TestSemantics( |
| flags: <SemanticsFlag>[SemanticsFlag.isButton], |
| actions: <SemanticsAction>[SemanticsAction.tap], |
| label: 'Dismiss', |
| textDirection: TextDirection.ltr, |
| ), |
| TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| TestSemantics( |
| children: <TestSemantics>[ |
| TestSemantics( |
| actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss], |
| label: 'Scrim', |
| hintOverrides: const SemanticsHintOverrides(onTapHint: 'Close Bottom Sheet'), |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreTransform: true, |
| ignoreRect: true, |
| ignoreId: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Drag handle color can take WidgetStateProperty', (WidgetTester tester) async { |
| const Color defaultColor = Colors.blue; |
| const Color hoveringColor = Colors.green; |
| |
| Future<void> checkDragHandleAndColors() async { |
| await tester.pump(); // bottom sheet show animation starts |
| await tester.pump(const Duration(seconds: 1)); // animation done |
| |
| final Finder dragHandle = find.bySemanticsLabel('Dismiss'); |
| expect(tester.getSize(dragHandle), const Size(48, 48)); |
| final Offset center = tester.getCenter(dragHandle); |
| final Offset edge = tester.getTopLeft(dragHandle) - const Offset(1, 1); |
| |
| // Shows default drag handle color |
| final TestGesture gesture = await tester.createGesture( |
| kind: PointerDeviceKind.mouse, |
| pointer: 1, |
| ); |
| await gesture.addPointer(location: edge); |
| await tester.pump(); |
| var boxDecoration = |
| tester |
| .widget<Container>( |
| find.descendant( |
| of: dragHandle, |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is Container && widget.decoration != null, |
| ), |
| ), |
| ) |
| .decoration! |
| as BoxDecoration; |
| expect(boxDecoration.color, defaultColor); |
| |
| // Shows hovering drag handle color |
| await gesture.moveTo(center); |
| await tester.pump(); |
| boxDecoration = |
| tester |
| .widget<Container>( |
| find.descendant( |
| of: dragHandle, |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is Container && widget.decoration != null, |
| ), |
| ), |
| ) |
| .decoration! |
| as BoxDecoration; |
| |
| expect(boxDecoration.color, hoveringColor); |
| await gesture.removePointer(); |
| } |
| |
| Widget buildScaffold(GlobalKey scaffoldKey) { |
| return MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: BottomSheetThemeData( |
| dragHandleColor: WidgetStateColor.resolveWith((Set<WidgetState> states) { |
| if (states.contains(WidgetState.hovered)) { |
| return hoveringColor; |
| } |
| return defaultColor; |
| }), |
| ), |
| ), |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ); |
| } |
| |
| var scaffoldKey = GlobalKey<ScaffoldState>(); |
| await tester.pumpWidget(buildScaffold(scaffoldKey)); |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| showDragHandle: true, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await checkDragHandleAndColors(); |
| |
| await tester.pumpWidget(Container()); // Reset |
| scaffoldKey = GlobalKey<ScaffoldState>(); |
| await tester.pumpWidget(buildScaffold(scaffoldKey)); |
| |
| scaffoldKey.currentState!.showBottomSheet((_) { |
| return Builder( |
| builder: (BuildContext context) { |
| return const SizedBox(height: 200.0, child: Text('Bottom Sheet')); |
| }, |
| ); |
| }, showDragHandle: true); |
| |
| await checkDragHandleAndColors(); |
| }); |
| |
| testWidgets('Drag handle interactive area size at minimum possible size', ( |
| WidgetTester tester, |
| ) async { |
| Widget buildScaffold(GlobalKey scaffoldKey, {Size? dragHandleSize}) { |
| return MaterialApp( |
| theme: ThemeData(bottomSheetTheme: BottomSheetThemeData(dragHandleSize: dragHandleSize)), |
| home: Scaffold(key: scaffoldKey), |
| ); |
| } |
| |
| const smallerDragHandleSize = Size(20, 20); |
| |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| await tester.pumpWidget(buildScaffold(scaffoldKey, dragHandleSize: smallerDragHandleSize)); |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| showDragHandle: true, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pump(); // Bottom sheet show animation starts. |
| await tester.pump(const Duration(seconds: 1)); // Animation done. |
| |
| final Finder dragHandle = find.bySemanticsLabel('Dismiss'); |
| expect( |
| tester.getSize(dragHandle), |
| const Size(kMinInteractiveDimension, kMinInteractiveDimension), |
| ); |
| }); |
| |
| testWidgets('Drag handle interactive area size at given dragHandleSize', ( |
| WidgetTester tester, |
| ) async { |
| Widget buildScaffold(GlobalKey scaffoldKey, {Size? dragHandleSize}) { |
| return MaterialApp( |
| theme: ThemeData(bottomSheetTheme: BottomSheetThemeData(dragHandleSize: dragHandleSize)), |
| home: Scaffold(key: scaffoldKey), |
| ); |
| } |
| |
| const extendedDragHandleSize = Size(100, 50); |
| |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| await tester.pumpWidget(buildScaffold(scaffoldKey, dragHandleSize: extendedDragHandleSize)); |
| |
| showModalBottomSheet<void>( |
| context: scaffoldKey.currentContext!, |
| showDragHandle: true, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| |
| await tester.pump(); // Bottom sheet show animation starts. |
| await tester.pump(const Duration(seconds: 1)); // Animation done. |
| |
| final Finder dragHandle = find.bySemanticsLabel('Dismiss'); |
| expect(tester.getSize(dragHandle), extendedDragHandleSize); |
| }); |
| |
| 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. |
| final double tabBarHeight = tester.getSize(find.byType(BottomNavigationBar)).height; |
| expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600 - tabBarHeight); |
| }); |
| |
| 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 scaffoldKey = GlobalKey<ScaffoldState>(); |
| const 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.settingsOf(context)!; |
| 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 tapTarget = Key('tap-target'); |
| final controller = AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ); |
| addTearDown(controller.dispose); |
| |
| 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); |
| }); |
| |
| // 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 tapTarget = Key('tap-target'); |
| final 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 tapTarget = Key('tap-target'); |
| const tapTargetToClose = Key('tap-target-to-close'); |
| final controller = AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ); |
| addTearDown(controller.dispose); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| key: tapTarget, |
| onTap: () { |
| showBottomSheet( |
| context: context, |
| // The default duration and reverseDuration is 1 second |
| transitionAnimationController: controller, |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| key: tapTargetToClose, |
| onPressed: () => Navigator.pop(context), |
| 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((_) { |
| 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((_) { |
| 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? 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((_) { |
| 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((_) { |
| 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 tapTarget = Key('tap-target'); |
| const tapTargetToClose = Key('tap-target-to-close'); |
| final 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( |
| context: context, |
| transitionAnimationController: controller, |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| key: tapTargetToClose, |
| onPressed: () => Navigator.pop(context), |
| 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( |
| 'The framework removes all animation listeners from foreign controllers when disposing.', |
| (WidgetTester tester) async { |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| final controller = _StatusTestAnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(seconds: 2), |
| reverseDuration: const Duration(seconds: 2), |
| ); |
| addTearDown(controller.dispose); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(controller.isListening, isFalse); |
| |
| scaffoldKey.currentState!.showBottomSheet((BuildContext context) { |
| return const SizedBox(height: 200.0, child: Text('BottomSheet')); |
| }, transitionAnimationController: controller); |
| |
| await tester.pumpAndSettle(); |
| expect(controller.isListening, isTrue); |
| 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(controller.isListening, isFalse); |
| expect(find.text('BottomSheet'), findsNothing); |
| }, |
| ); |
| |
| 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? 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((BuildContext context) => const Text('BottomSheet 1')); |
| }, |
| ), |
| ElevatedButton( |
| child: const Text('show 2'), |
| onPressed: () { |
| Scaffold.of( |
| context, |
| ).showBottomSheet((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); |
| }, |
| ); |
| |
| testWidgets('ModalBottomSheetRoute shows BottomSheet correctly', (WidgetTester tester) async { |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(find.byType(BottomSheet), findsNothing); |
| |
| // Bring up bottom sheet. |
| final NavigatorState navigator = Navigator.of(savedContext); |
| navigator.push( |
| ModalBottomSheetRoute<void>( |
| isScrollControlled: false, |
| builder: (BuildContext context) => Container(), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(find.byType(BottomSheet), 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('Material3 - Default constraints are max width 640', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: MediaQuery( |
| data: MediaQueryData(size: Size(1000, 1000)), |
| child: Scaffold( |
| body: Center(child: Text('body')), |
| bottomSheet: Placeholder(fallbackWidth: 800), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(Placeholder)).width, 640); |
| }); |
| |
| testWidgets('Material2 - No constraints by default for bottomSheet property', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| // This test is specific to Material2 because Material3 sets constraints by default for BottomSheet. |
| theme: ThemeData(useMaterial3: false), |
| home: const 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( |
| // This test is specific to Material2 because Material3 sets constraints by default for BottomSheet. |
| theme: ThemeData(useMaterial3: false), |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| Scaffold.of( |
| context, |
| ).showBottomSheet((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( |
| // This test is specific to Material2 because Material3 sets constraints by default for BottomSheet. |
| theme: ThemeData(useMaterial3: false), |
| 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('Material3 - Theme constraints used for bottomSheet property', ( |
| WidgetTester tester, |
| ) async { |
| const sheetMaxWidth = 80.0; |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: sheetMaxWidth), |
| ), |
| ), |
| 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. |
| final Rect bottomSheetRect = tester.getRect(find.text('BottomSheet')); |
| expect(bottomSheetRect.left, 800 / 2 - sheetMaxWidth / 2); |
| expect(bottomSheetRect.width, sheetMaxWidth); |
| |
| // Ensure the FAB is overlapping the top of the sheet. |
| expect(find.byIcon(Icons.add), findsOneWidget); |
| final Rect iconRect = tester.getRect(find.byIcon(Icons.add)); |
| expect(iconRect.top, bottomSheetRect.top - iconRect.height / 2); |
| }); |
| |
| testWidgets('Material2 - Theme constraints used for bottomSheet property', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| useMaterial3: false, |
| 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 { |
| const sheetMaxWidth = 80.0; |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: sheetMaxWidth), |
| ), |
| ), |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| child: const Text('Press me'), |
| onPressed: () { |
| Scaffold.of( |
| context, |
| ).showBottomSheet((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. |
| final Rect bottomSheetRect = tester.getRect(find.text('BottomSheet')); |
| expect(bottomSheetRect.left, 800 / 2 - sheetMaxWidth / 2); |
| expect(bottomSheetRect.width, sheetMaxWidth); |
| }); |
| |
| testWidgets('Theme constraints used for showModalBottomSheet', (WidgetTester tester) async { |
| const sheetMaxWidth = 80.0; |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| bottomSheetTheme: const BottomSheetThemeData( |
| constraints: BoxConstraints(maxWidth: sheetMaxWidth), |
| ), |
| ), |
| 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. |
| final Rect bottomSheetRect = tester.getRect(find.text('BottomSheet')); |
| expect(bottomSheetRect.left, 800 / 2 - sheetMaxWidth / 2); |
| expect(bottomSheetRect.width, sheetMaxWidth); |
| }); |
| |
| testWidgets('constraints param overrides theme for showBottomSheet', ( |
| WidgetTester tester, |
| ) async { |
| const sheetMaxWidth = 100.0; |
| 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( |
| (BuildContext context) => const Text('BottomSheet'), |
| constraints: const BoxConstraints(maxWidth: sheetMaxWidth), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| 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. |
| final Rect bottomSheetRect = tester.getRect(find.text('BottomSheet')); |
| expect(bottomSheetRect.left, 800 / 2 - sheetMaxWidth / 2); |
| expect(bottomSheetRect.width, sheetMaxWidth); |
| }); |
| |
| testWidgets('constraints param overrides theme for showModalBottomSheet', ( |
| WidgetTester tester, |
| ) async { |
| const sheetMaxWidth = 100.0; |
| 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: sheetMaxWidth), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| 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. |
| final Rect bottomSheetRect = tester.getRect(find.text('BottomSheet')); |
| expect(bottomSheetRect.left, 800 / 2 - sheetMaxWidth / 2); |
| expect(bottomSheetRect.width, sheetMaxWidth); |
| }); |
| |
| group('scrollControlDisabledMaxHeightRatio', () { |
| Future<void> test( |
| WidgetTester tester, |
| bool isScrollControlled, |
| double scrollControlDisabledMaxHeightRatio, |
| ) 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, |
| isScrollControlled: isScrollControlled, |
| scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio, |
| builder: (BuildContext context) => |
| const SizedBox.expand(child: Text('BottomSheet')), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| await tester.tap(find.text('Press me')); |
| await tester.pumpAndSettle(); |
| expect( |
| tester.getRect(find.text('BottomSheet')), |
| Rect.fromLTRB( |
| 80, |
| 600 * (isScrollControlled ? 0 : (1 - scrollControlDisabledMaxHeightRatio)), |
| 720, |
| 600, |
| ), |
| ); |
| } |
| |
| testWidgets('works at 9 / 16', (WidgetTester tester) { |
| return test(tester, false, 9.0 / 16.0); |
| }); |
| testWidgets('works at 8 / 16', (WidgetTester tester) { |
| return test(tester, false, 8.0 / 16.0); |
| }); |
| testWidgets('works at isScrollControlled', (WidgetTester tester) { |
| return test(tester, true, 8.0 / 16.0); |
| }); |
| }); |
| }); |
| |
| group('showModalBottomSheet modalBarrierDismissLabel', () { |
| testWidgets('Verify that modalBarrierDismissLabel is used if provided', ( |
| WidgetTester tester, |
| ) async { |
| final scaffoldKey = GlobalKey<ScaffoldState>(); |
| const customLabel = 'custom label'; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| key: scaffoldKey, |
| body: const Center(child: Text('body')), |
| ), |
| ), |
| ); |
| |
| showModalBottomSheet<void>( |
| barrierLabel: 'custom label', |
| context: scaffoldKey.currentContext!, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); |
| expect(modalBarrier.semanticsLabel, customLabel); |
| }); |
| |
| testWidgets( |
| 'Verify that modalBarrierDismissLabel from context is used if barrierLabel is not provided', |
| (WidgetTester tester) async { |
| final 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(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); |
| expect( |
| modalBarrier.semanticsLabel, |
| MaterialLocalizations.of(scaffoldKey.currentContext!).scrimLabel, |
| ); |
| }, |
| ); |
| }); |
| |
| testWidgets('Bottom sheet animation can be customized', (WidgetTester tester) async { |
| final Key sheetKey = UniqueKey(); |
| |
| Widget buildWidget({AnimationStyle? sheetAnimationStyle}) { |
| return MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| onTap: () { |
| showBottomSheet( |
| context: context, |
| sheetAnimationStyle: sheetAnimationStyle, |
| builder: (BuildContext context) { |
| return SizedBox.expand( |
| child: ColoredBox( |
| key: sheetKey, |
| color: Theme.of(context).colorScheme.primary, |
| child: FilledButton( |
| onPressed: () { |
| Navigator.pop(context); |
| }, |
| child: const Text('Close'), |
| ), |
| ), |
| ); |
| }, |
| ); |
| }, |
| child: const Text('X'), |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| // Test custom animation style. |
| await tester.pumpWidget( |
| buildWidget( |
| sheetAnimationStyle: const AnimationStyle( |
| duration: Duration(milliseconds: 800), |
| reverseDuration: Duration(milliseconds: 400), |
| ), |
| ), |
| ); |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the custom forward duration. |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| // The bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1)); |
| |
| // Advance the animation by 1/2 of the custom forward duration. |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| // The bottom sheet is fully visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0)); |
| |
| // Dismiss the bottom sheet. |
| await tester.tap(find.widgetWithText(FilledButton, 'Close')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the custom reverse duration. |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| // The bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1)); |
| |
| // Advance the animation by 1/2 of the custom reverse duration. |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| // The bottom sheet is dismissed. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0)); |
| |
| // Test no animation style. |
| await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| |
| // The bottom sheet is fully visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0)); |
| |
| // Dismiss the bottom sheet. |
| await tester.tap(find.widgetWithText(FilledButton, 'Close')); |
| await tester.pump(); |
| |
| // The bottom sheet is dismissed. |
| expect(find.byKey(sheetKey), findsNothing); |
| }); |
| |
| testWidgets('Modal bottom sheet default animation', (WidgetTester tester) async { |
| final Key sheetKey = UniqueKey(); |
| |
| // Test default modal bottom sheet animation. |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| onTap: () { |
| showModalBottomSheet<void>( |
| context: context, |
| builder: (BuildContext context) { |
| return SizedBox.expand( |
| child: ColoredBox( |
| key: sheetKey, |
| color: Theme.of(context).colorScheme.primary, |
| child: FilledButton( |
| onPressed: () { |
| Navigator.pop(context); |
| }, |
| child: const Text('Close'), |
| ), |
| ), |
| ); |
| }, |
| ); |
| }, |
| child: const Text('X'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap the 'X' to show the bottom sheet. |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the default forward duration. |
| await tester.pump(const Duration(milliseconds: 125)); |
| |
| // The modal bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1)); |
| |
| // Advance the animation by 1/2 of the default forward duration. |
| await tester.pump(const Duration(milliseconds: 125)); |
| |
| // The modal bottom sheet is fully visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5)); |
| |
| // Dismiss the bottom sheet. |
| await tester.tap(find.widgetWithText(FilledButton, 'Close')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the default reverse duration. |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // The modal bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1)); |
| |
| // Advance the animation by 1/2 of the default reverse duration. |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // The modal bottom sheet is dismissed. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0)); |
| }); |
| |
| testWidgets('Modal bottom sheet animation can be customized', (WidgetTester tester) async { |
| final Key sheetKey = UniqueKey(); |
| |
| Widget buildWidget({AnimationStyle? sheetAnimationStyle}) { |
| return MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return GestureDetector( |
| onTap: () { |
| showModalBottomSheet<void>( |
| context: context, |
| sheetAnimationStyle: sheetAnimationStyle, |
| builder: (BuildContext context) { |
| return SizedBox.expand( |
| child: ColoredBox( |
| key: sheetKey, |
| color: Theme.of(context).colorScheme.primary, |
| child: FilledButton( |
| onPressed: () { |
| Navigator.pop(context); |
| }, |
| child: const Text('Close'), |
| ), |
| ), |
| ); |
| }, |
| ); |
| }, |
| child: const Text('X'), |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| // Test custom animation style. |
| await tester.pumpWidget( |
| buildWidget( |
| sheetAnimationStyle: const AnimationStyle( |
| duration: Duration(milliseconds: 800), |
| reverseDuration: Duration(milliseconds: 400), |
| ), |
| ), |
| ); |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the custom forward duration. |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| // The bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1)); |
| |
| // Advance the animation by 1/2 of the custom forward duration. |
| await tester.pump(const Duration(milliseconds: 400)); |
| |
| // The bottom sheet is fully visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5)); |
| |
| // Dismiss the bottom sheet. |
| await tester.tap(find.widgetWithText(FilledButton, 'Close')); |
| await tester.pump(); |
| // Advance the animation by 1/2 of the custom reverse duration. |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| // The bottom sheet is partially visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1)); |
| |
| // Advance the animation by 1/2 of the custom reverse duration. |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| // The bottom sheet is dismissed. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0)); |
| |
| // Test no animation style. |
| await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| |
| // The bottom sheet is fully visible. |
| expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5)); |
| |
| // Dismiss the bottom sheet. |
| await tester.tap(find.widgetWithText(FilledButton, 'Close')); |
| await tester.pump(); |
| |
| // The bottom sheet is dismissed. |
| expect(find.byKey(sheetKey), findsNothing); |
| }); |
| |
| testWidgets( |
| 'Setting ModalBottomSheetRoute.requestFocus to false does not request focus on the bottom sheet', |
| (WidgetTester tester) async { |
| late BuildContext savedContext; |
| final focusNode = FocusNode(); |
| addTearDown(focusNode.dispose); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return TextField(focusNode: focusNode); |
| }, |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| |
| FocusNode? getTextFieldFocusNode() { |
| return tester |
| .widget<Focus>( |
| find.descendant(of: find.byType(TextField), matching: find.byType(Focus)), |
| ) |
| .focusNode; |
| } |
| |
| // Initially, there is no bottom sheet and the text field has no focus. |
| expect(find.byType(BottomSheet), findsNothing); |
| expect(getTextFieldFocusNode()?.hasFocus, false); |
| |
| // Request focus on the text field. |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(getTextFieldFocusNode()?.hasFocus, true); |
| |
| // Bring up bottom sheet. |
| final NavigatorState navigator = Navigator.of(savedContext); |
| navigator.push( |
| ModalBottomSheetRoute<void>( |
| isScrollControlled: false, |
| builder: (BuildContext context) => Container(), |
| ), |
| ); |
| await tester.pump(); |
| |
| // The bottom sheet is showing and the text field has lost focus. |
| expect(find.byType(BottomSheet), findsOneWidget); |
| expect(getTextFieldFocusNode()?.hasFocus, false); |
| |
| // Dismiss the bottom sheet. |
| navigator.pop(); |
| await tester.pump(); |
| |
| // The bottom sheet is dismissed and the focus is shifted back to the text field. |
| expect(find.byType(BottomSheet), findsNothing); |
| expect(getTextFieldFocusNode()?.hasFocus, true); |
| |
| // Bring up bottom sheet again with requestFocus to false. |
| navigator.push( |
| ModalBottomSheetRoute<void>( |
| requestFocus: false, |
| isScrollControlled: false, |
| builder: (BuildContext context) => Container(), |
| ), |
| ); |
| await tester.pump(); |
| |
| // The bottom sheet is showing and the text field still has focus. |
| expect(find.byType(BottomSheet), findsOneWidget); |
| expect(getTextFieldFocusNode()?.hasFocus, true); |
| }, |
| ); |
| |
| testWidgets('requestFocus works correctly in showModalBottomSheet.', (WidgetTester tester) async { |
| final navigatorKey = GlobalKey<NavigatorState>(); |
| final focusNode = FocusNode(); |
| addTearDown(focusNode.dispose); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigatorKey, |
| home: Scaffold(body: TextField(focusNode: focusNode)), |
| ), |
| ); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(focusNode.hasFocus, true); |
| |
| showModalBottomSheet<void>( |
| context: navigatorKey.currentContext!, |
| requestFocus: true, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| await tester.pumpAndSettle(); |
| expect(FocusScope.of(tester.element(find.text('BottomSheet'))).hasFocus, true); |
| expect(focusNode.hasFocus, false); |
| |
| navigatorKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(focusNode.hasFocus, true); |
| |
| showModalBottomSheet<void>( |
| context: navigatorKey.currentContext!, |
| requestFocus: false, |
| builder: (BuildContext context) => const Text('BottomSheet'), |
| ); |
| await tester.pumpAndSettle(); |
| expect(FocusScope.of(tester.element(find.text('BottomSheet'))).hasFocus, false); |
| expect(focusNode.hasFocus, true); |
| }); |
| |
| testWidgets('BottomSheet does not crash at zero area', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: SizedBox.shrink( |
| child: BottomSheet( |
| onClosing: () {}, |
| builder: (BuildContext context) => const Text('X'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(BottomSheet)), Size.zero); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/177004 |
| testWidgets('ModalBottomSheet semantics for mismatched platforms', (WidgetTester tester) async { |
| const localizations = DefaultMaterialLocalizations(); |
| |
| Future<void> pumpModalBottomSheetWithTheme(TargetPlatform themePlatform) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: themePlatform), |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { |
| return OutlinedButton( |
| onPressed: () { |
| showModalBottomSheet<void>( |
| context: context, |
| showDragHandle: true, |
| builder: (BuildContext context) { |
| return const Text('BottomSheet'); |
| }, |
| ); |
| }, |
| child: const Text('open'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('open')); |
| await tester.pumpAndSettle(); |
| |
| final Finder popupFinder = find.bySemanticsLabel(localizations.dialogLabel); |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| expect(popupFinder, findsNothing); // Apple platforms don't show label. |
| case _: |
| expect(popupFinder, findsOneWidget); // Non-Apple platforms show label. |
| } |
| } |
| |
| // Test with theme.platform = Android on different real platforms. |
| await pumpModalBottomSheetWithTheme(TargetPlatform.android); |
| |
| // Dismiss the first bottom sheet. |
| Navigator.of(tester.element(find.text('BottomSheet'))).pop(); |
| await tester.pumpAndSettle(); |
| |
| // Test with theme.platform = iOS on different real platforms. |
| await pumpModalBottomSheetWithTheme(TargetPlatform.iOS); |
| }, variant: TargetPlatformVariant.all()); |
| |
| testWidgets('Modal bottom sheet has hitTestBehavior.opaque to prevent dismissal on empty areas', ( |
| WidgetTester tester, |
| ) async { |
| final semantics = SemanticsTester(tester); |
| late BuildContext savedContext; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Builder( |
| builder: (BuildContext context) { |
| savedContext = context; |
| return Container(); |
| }, |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| |
| showModalBottomSheet<void>( |
| context: savedContext, |
| builder: (BuildContext context) => Container( |
| height: 200, |
| color: Colors.blue, |
| child: const Center(child: Text('Modal Bottom Sheet')), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Modal Bottom Sheet'), findsOneWidget); |
| |
| // Verify the route-level Semantics has opaque hitTestBehavior |
| // This prevents clicks inside the bottom sheet from passing through to the barrier |
| final List<Semantics> allSemantics = tester |
| .widgetList<Semantics>( |
| find.ancestor(of: find.text('Modal Bottom Sheet'), matching: find.byType(Semantics)), |
| ) |
| .toList(); |
| |
| final Semantics routeSemantics = allSemantics.firstWhere( |
| (Semantics s) => s.properties.hitTestBehavior == SemanticsHitTestBehavior.opaque, |
| ); |
| |
| expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque); |
| |
| final Semantics widgetSemantics = allSemantics.firstWhere( |
| (Semantics s) => s.properties.scopesRoute ?? false, |
| ); |
| |
| expect(widgetSemantics.properties.scopesRoute, true); |
| |
| semantics.dispose(); |
| }); |
| } |
| |
| 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'), |
| ); |
| } |
| }, |
| ), |
| ); |
| } |
| } |
| |
| class _StatusTestAnimationController extends AnimationController with AnimationLazyListenerMixin { |
| _StatusTestAnimationController({super.duration, super.reverseDuration, required super.vsync}); |
| |
| @override |
| void didStartListening() {} |
| |
| @override |
| void didStopListening() {} |
| } |