flutter/packages/flutter/test/material/bottom_sheet_test.dart
Kishan Rathore f6144f0c25
feat: Add drag handle size to be configurable based on given size (#152085)
Feat: Drag handle size can now be changed to any given size.
So, In previous behaviour drag handle size was not able to extends beyond 48x48. But some user might want to change it.
In current behaviour,  drag handle size is default to same 48x48 but if drag handle size grows beyond that, we don't restrict it.

Fixes #149170
2024-07-26 16:15:36 +00:00

2642 lines
88 KiB
Dart

// 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((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((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((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 GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
int 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();
},
),
));
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);
});
// 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);
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((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((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 SafeArea 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 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(
children: <TestSemantics>[
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
label: 'Scrim',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
), 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('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,
),
useMaterial3: true,
),
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(
theme: ThemeData(
useMaterial3: true,
),
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 SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<ScaffoldState> 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(
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',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Material3 - Modal BottomSheet with ScrollController has semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(useMaterial3: true),
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(
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',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Material3 - Modal BottomSheet with drag handle has semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(useMaterial3: true),
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(
label: 'Dialog',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
children: <TestSemantics>[
TestSemantics(
label: 'BottomSheet',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Dismiss',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
label: 'Scrim',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Drag handle color can take MaterialStateProperty', (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();
BoxDecoration 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.light().copyWith(
bottomSheetTheme: BottomSheetThemeData(
dragHandleColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveringColor;
}
return defaultColor;
}),
),
),
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
);
}
GlobalKey<ScaffoldState> 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.light().copyWith(
bottomSheetTheme: BottomSheetThemeData(
dragHandleSize: dragHandleSize
),
),
home: Scaffold(
key: scaffoldKey,
),
);
}
const Size smallerDragHandleSize = Size(20, 20);
final GlobalKey<ScaffoldState> 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.light().copyWith(
bottomSheetTheme: BottomSheetThemeData(
dragHandleSize: dragHandleSize
),
),
home: Scaffold(
key: scaffoldKey,
),
);
}
const Size extendedDragHandleSize = Size(100, 50);
final GlobalKey<ScaffoldState> 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 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.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 Key tapTarget = Key('tap-target');
final AnimationController 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 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');
final AnimationController 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 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(
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('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(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const 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 double sheetMaxWidth = 80.0;
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
useMaterial3: true,
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 double 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 double 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 double 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 double 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 GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
const String 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 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();
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: AnimationStyle(
duration: const Duration(milliseconds: 800),
reverseDuration: const 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: AnimationStyle(
duration: const Duration(milliseconds: 800),
reverseDuration: const 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);
});
}
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'),
);
}
},
),
);
}
}