blob: 0c30a9ff07ac82d31cb3121c3aee648d3e7c0af9 [file] [log] [blame]
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.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('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'),
isDismissible: true,
).then<void>((void value) {
showBottomSheetThenCalled = true;
});
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(showBottomSheetThenCalled, isFalse);
// Tap above the bottom sheet to dismiss it.
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpAndSettle(); // Bottom sheet dismiss animation.
expect(showBottomSheetThenCalled, isTrue);
expect(find.text('BottomSheet'), findsNothing);
});
testWidgets('Verify that the BottomSheet animates non-linearly', (WidgetTester tester) async {
late BuildContext savedContext;
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
savedContext = context;
return Container();
},
),
));
await tester.pump();
expect(find.text('BottomSheet'), findsNothing);
showModalBottomSheet<void>(
context: savedContext,
builder: (BuildContext context) => const Text('BottomSheet'),
);
await tester.pump();
await _checkNonLinearAnimation(tester);
await tester.pumpAndSettle();
// Tap above the bottom sheet to dismiss it.
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
await _checkNonLinearAnimation(tester);
await tester.pumpAndSettle(); // Bottom sheet dismiss animation.
expect(find.text('BottomSheet'), findsNothing);
});
testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async {
late BuildContext savedContext;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
savedContext = context;
return Container();
},
),
),
);
await tester.pump();
expect(find.text('BottomSheet'), findsNothing);
bool showBottomSheetThenCalled = false;
showModalBottomSheet<void>(
context: savedContext,
builder: (BuildContext context) => const Text('BottomSheet'),
isDismissible: false,
).then<void>((void value) {
showBottomSheetThenCalled = true;
});
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(showBottomSheetThenCalled, isFalse);
// Tap above the bottom sheet, attempting to dismiss it.
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpAndSettle(); // Bottom sheet should not dismiss.
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsOneWidget);
});
testWidgets('Swiping down a modal BottomSheet should dismiss it by default', (WidgetTester tester) async {
late BuildContext savedContext;
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
savedContext = context;
return Container();
},
),
));
await tester.pump();
expect(find.text('BottomSheet'), findsNothing);
bool showBottomSheetThenCalled = false;
showModalBottomSheet<void>(
context: savedContext,
isDismissible: false,
builder: (BuildContext context) => const Text('BottomSheet'),
).then<void>((void value) {
showBottomSheetThenCalled = true;
});
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(showBottomSheetThenCalled, isFalse);
// Swipe the bottom sheet to dismiss it.
await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0));
await tester.pumpAndSettle(); // Bottom sheet dismiss animation.
expect(showBottomSheetThenCalled, isTrue);
expect(find.text('BottomSheet'), findsNothing);
});
testWidgets('Swiping down a modal BottomSheet should not dismiss it when enableDrag is false', (WidgetTester tester) async {
late BuildContext savedContext;
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
savedContext = context;
return Container();
},
),
));
await tester.pump();
expect(find.text('BottomSheet'), findsNothing);
bool showBottomSheetThenCalled = false;
showModalBottomSheet<void>(
context: savedContext,
isDismissible: false,
enableDrag: false,
builder: (BuildContext context) => const Text('BottomSheet'),
).then<void>((void value) {
showBottomSheetThenCalled = true;
});
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(showBottomSheetThenCalled, isFalse);
// Swipe the bottom sheet, attempting to dismiss it.
await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0));
await tester.pumpAndSettle(); // Bottom sheet should not dismiss.
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsOneWidget);
});
testWidgets('Swiping down a modal BottomSheet should dismiss it when enableDrag is true', (WidgetTester tester) async {
late BuildContext savedContext;
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
savedContext = context;
return Container();
},
),
));
await tester.pump();
expect(find.text('BottomSheet'), findsNothing);
bool showBottomSheetThenCalled = false;
showModalBottomSheet<void>(
context: savedContext,
isDismissible: false,
enableDrag: true,
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,
enableDrag: true,
builder: (BuildContext context) {
numBuilderCalls++;
return const Text('BottomSheet');
},
);
await tester.pumpAndSettle();
expect(numBuilderCalls, 1);
// Swipe the bottom sheet to dismiss it.
await tester.drag(find.text('BottomSheet'), const Offset(0.0, 150.0));
await tester.pumpAndSettle(); // Bottom sheet dismiss animation.
expect(numBuilderCalls, 1);
});
testWidgets('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
bool showBottomSheetThenCalled = false;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsNothing);
scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
return Container(
margin: const EdgeInsets.all(40.0),
child: const Text('BottomSheet'),
);
}).closed.whenComplete(() {
showBottomSheetThenCalled = true;
});
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsNothing);
await tester.pump(); // bottom sheet show animation starts
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsOneWidget);
await tester.pump(const Duration(seconds: 1)); // animation done
expect(showBottomSheetThenCalled, isFalse);
expect(find.text('BottomSheet'), findsOneWidget);
// The fling below must be such that the velocity estimation examines an
// offset greater than the kTouchSlop. Too slow or too short a distance, and
// it won't trigger. Also, it must not be so much that it drags the bottom
// sheet off the screen, or we won't see it after we pump!
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 50.0), 2000.0);
await tester.pump(); // drain the microtask queue (Future completion callback)
expect(showBottomSheetThenCalled, isTrue);
expect(find.text('BottomSheet'), findsOneWidget);
await tester.pump(); // bottom sheet dismiss animation starts
expect(showBottomSheetThenCalled, isTrue);
expect(find.text('BottomSheet'), findsOneWidget);
await tester.pump(const Duration(seconds: 1)); // animation done
expect(showBottomSheetThenCalled, isTrue);
expect(find.text('BottomSheet'), findsNothing);
});
testWidgets('Verify that dragging past the bottom dismisses a persistent BottomSheet', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/5528
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
return Container(
margin: const EdgeInsets.all(40.0),
child: const Text('BottomSheet'),
);
});
await tester.pump(); // bottom sheet show animation starts
await tester.pump(const Duration(seconds: 1)); // animation done
expect(find.text('BottomSheet'), findsOneWidget);
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 400.0), 1000.0);
await tester.pump(); // drain the microtask queue (Future completion callback)
await tester.pump(); // bottom sheet dismiss animation starts
await tester.pump(const Duration(seconds: 1)); // animation done
expect(find.text('BottomSheet'), findsNothing);
});
testWidgets('modal BottomSheet has no top MediaQuery', (WidgetTester tester) async {
late BuildContext outerContext;
late BuildContext innerContext;
await tester.pumpWidget(Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.all(50.0),
size: Size(400.0, 600.0),
),
child: Navigator(
onGenerateRoute: (_) {
return PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
outerContext = context;
return Container();
},
);
},
),
),
),
));
showModalBottomSheet<void>(
context: outerContext,
builder: (BuildContext context) {
innerContext = context;
return Container();
},
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(
MediaQuery.of(outerContext)!.padding,
const EdgeInsets.all(50.0),
);
expect(
MediaQuery.of(innerContext)!.padding,
const EdgeInsets.only(left: 50.0, right: 50.0, bottom: 50.0),
);
});
testWidgets('modal BottomSheet 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 Container(
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(
label: 'BottomSheet',
textDirection: TextDirection.ltr,
),
],
),
],
),
TestSemantics(),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
const Color color = Colors.pink;
const double elevation = 9.0;
final ShapeBorder shape = BeveledRectangleBorder(borderRadius: BorderRadius.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 Container(
child: const Text('BottomSheet'),
);
},
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final BottomSheet bottomSheet = tester.widget(find.byType(BottomSheet));
expect(bottomSheet.backgroundColor, color);
expect(bottomSheet.elevation, elevation);
expect(bottomSheet.shape, shape);
expect(bottomSheet.clipBehavior, clipBehavior);
final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last);
expect(modalBarrier.color, barrierColor);
});
testWidgets('modal BottomSheet with scrollController has semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
showModalBottomSheet<void>(
context: scaffoldKey.currentContext!,
builder: (BuildContext context) {
return DraggableScrollableSheet(
expand: false,
builder: (_, ScrollController controller) {
return SingleChildScrollView(
controller: controller,
child: Container(
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],
actions: <SemanticsAction>[SemanticsAction.scrollDown, SemanticsAction.scrollUp],
children: <TestSemantics>[
TestSemantics(
label: 'BottomSheet',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) {
return const _TestPage();
})),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
label: 'Item 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.style),
label: 'Item 2',
),
],
),
),
));
await tester.tap(find.text('Show bottom sheet'));
await tester.pumpAndSettle();
// Bottom sheet is displayed in correct position within the inner navigator
// and above the BottomNavigationBar.
expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 544.0);
});
testWidgets('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) {
return const _TestPage(useRootNavigator: true);
})),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
label: 'Item 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.style),
label: 'Item 2',
),
],
),
),
));
await tester.tap(find.text('Show bottom sheet'));
await tester.pumpAndSettle();
// Bottom sheet is displayed in correct position above all content including
// the BottomNavigationBar.
expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600.0);
});
testWidgets('Verify that route settings can be set in the showModalBottomSheet',
(WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
const RouteSettings routeSettings =
RouteSettings(name: 'route_name', arguments: 'route_argument');
await tester.pumpWidget(MaterialApp(
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
late RouteSettings retrievedRouteSettings;
showModalBottomSheet<void>(
context: scaffoldKey.currentContext!,
routeSettings: routeSettings,
builder: (BuildContext context) {
retrievedRouteSettings = ModalRoute.of(context)!.settings;
return Container(
child: const Text('BottomSheet'),
);
},
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(retrievedRouteSettings, routeSettings);
});
}
class _TestPage extends StatelessWidget {
const _TestPage({Key? key, this.useRootNavigator}) : super(key: key);
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'),
);
}
},
),
);
}
}