blob: 176d10d9b594dacdda0e4bb63affeddb5df63ebd [file] [log] [blame]
// Copyright 2015 The Chromium 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Scaffold control test', (WidgetTester tester) async {
final Key bodyKey = UniqueKey();
Widget boilerplate(Widget child) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: child,
),
);
}
await tester.pumpWidget(boilerplate(Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
),
));
expect(tester.takeException(), isFlutterError);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
),
));
RenderBox bodyBox = tester.renderObject(find.byKey(bodyKey));
expect(bodyBox.size, equals(const Size(800.0, 544.0)));
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)),
child: Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
),
),
));
bodyBox = tester.renderObject(find.byKey(bodyKey));
expect(bodyBox.size, equals(const Size(800.0, 444.0)));
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)),
child: Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
resizeToAvoidBottomInset: false,
),
)));
bodyBox = tester.renderObject(find.byKey(bodyKey));
expect(bodyBox.size, equals(const Size(800.0, 544.0)));
// Backwards compatiblity: deprecated resizeToAvoidBottomPadding flag
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)),
child: Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
resizeToAvoidBottomPadding: false,
),
)));
bodyBox = tester.renderObject(find.byKey(bodyKey));
expect(bodyBox.size, equals(const Size(800.0, 544.0)));
});
testWidgets('Scaffold large bottom padding test', (WidgetTester tester) async {
final Key bodyKey = UniqueKey();
Widget boilerplate(Widget child) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: child,
),
);
}
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 700.0),
),
child: Scaffold(
body: Container(key: bodyKey),
))
));
final RenderBox bodyBox = tester.renderObject(find.byKey(bodyKey));
expect(bodyBox.size, equals(const Size(800.0, 0.0)));
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 500.0),
),
child: Scaffold(
body: Container(key: bodyKey),
),
),
));
expect(bodyBox.size, equals(const Size(800.0, 100.0)));
await tester.pumpWidget(boilerplate(MediaQuery(
data: const MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 580.0),
),
child: Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Container(key: bodyKey),
),
),
));
expect(bodyBox.size, equals(const Size(800.0, 0.0)));
});
testWidgets('Floating action entrance/exit animation', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(
floatingActionButton: FloatingActionButton(
key: Key('one'),
onPressed: null,
child: Text('1'),
),
)));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(const MaterialApp(home: Scaffold(
floatingActionButton: FloatingActionButton(
key: Key('two'),
onPressed: null,
child: Text('2'),
),
)));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await tester.pumpWidget(Container());
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(const MaterialApp(home: Scaffold()));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(const MaterialApp(home: Scaffold(
floatingActionButton: FloatingActionButton(
key: Key('one'),
onPressed: null,
child: Text('1'),
),
)));
expect(tester.binding.transientCallbackCount, greaterThan(0));
});
testWidgets('Floating action button directionality', (WidgetTester tester) async {
Widget build(TextDirection textDirection) {
return Directionality(
textDirection: textDirection,
child: const MediaQuery(
data: MediaQueryData(
viewInsets: EdgeInsets.only(bottom: 200.0),
),
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: null,
child: Text('1'),
),
),
),
);
}
await tester.pumpWidget(build(TextDirection.ltr));
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));
await tester.pumpWidget(build(TextDirection.rtl));
expect(tester.binding.transientCallbackCount, 0);
expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 356.0));
});
testWidgets('Drawer scrolling', (WidgetTester tester) async {
final Key drawerKey = UniqueKey();
const double appBarHeight = 256.0;
final ScrollController scrollOffset = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
drawer: Drawer(
key: drawerKey,
child: ListView(
dragStartBehavior: DragStartBehavior.down,
controller: scrollOffset,
children: List<Widget>.generate(10,
(int index) => SizedBox(height: 100.0, child: Text('D$index')),
),
),
),
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: appBarHeight,
title: Text('Title'),
flexibleSpace: FlexibleSpaceBar(title: Text('Title')),
),
SliverPadding(
padding: const EdgeInsets.only(top: appBarHeight),
sliver: SliverList(
delegate: SliverChildListDelegate(List<Widget>.generate(
10, (int index) => SizedBox(height: 100.0, child: Text('B$index')),
)),
),
),
],
),
),
)
);
final ScaffoldState state = tester.firstState(find.byType(Scaffold));
state.openDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(scrollOffset.offset, 0.0);
const double scrollDelta = 80.0;
await tester.drag(find.byKey(drawerKey), const Offset(0.0, -scrollDelta));
await tester.pump();
expect(scrollOffset.offset, scrollDelta);
final RenderBox renderBox = tester.renderObject(find.byType(AppBar));
expect(renderBox.size.height, equals(appBarHeight));
});
Widget _buildStatusBarTestApp(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(top: 25.0)), // status bar
child: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
const SliverAppBar(
title: Text('Title'),
),
SliverList(
delegate: SliverChildListDelegate(List<Widget>.generate(
20, (int index) => SizedBox(height: 100.0, child: Text('$index')),
)),
),
],
),
),
),
);
}
testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async {
await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.iOS));
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
scrollable.position.jumpTo(500.0);
expect(scrollable.position.pixels, equals(500.0));
await tester.tapAt(const Offset(100.0, 10.0));
await tester.pumpAndSettle();
expect(scrollable.position.pixels, equals(0.0));
});
testWidgets('Tapping the status bar does not scroll to top on Android', (WidgetTester tester) async {
await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.android));
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
scrollable.position.jumpTo(500.0);
expect(scrollable.position.pixels, equals(500.0));
await tester.tapAt(const Offset(100.0, 10.0));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(scrollable.position.pixels, equals(500.0));
});
testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showBottomSheet<void>((BuildContext context) {
return Container(
key: sheetKey,
color: Colors.blue[500],
);
});
},
child: const Text('X'),
);
},
),
),
),
);
await tester.tap(find.text('X'));
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1));
final RenderBox appBarBox = tester.renderObject(find.byType(AppBar));
final RenderBox sheetBox = tester.renderObject(find.byKey(sheetKey));
final Offset appBarBottomRight = appBarBox.localToGlobal(appBarBox.size.bottomRight(Offset.zero));
final Offset sheetTopRight = sheetBox.localToGlobal(sheetBox.size.topRight(Offset.zero));
expect(appBarBottomRight, equals(sheetTopRight));
});
testWidgets('Persistent bottom buttons are persistent', (WidgetTester tester) async {
bool didPressButton = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Container(
color: Colors.amber[500],
height: 5000.0,
child: const Text('body'),
),
),
persistentFooterButtons: <Widget>[
FlatButton(
onPressed: () {
didPressButton = true;
},
child: const Text('X'),
),
],
),
),
);
await tester.drag(find.text('body'), const Offset(0.0, -1000.0));
expect(didPressButton, isFalse);
await tester.tap(find.text('X'));
expect(didPressButton, isTrue);
});
testWidgets('Persistent bottom buttons apply media padding', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
),
child: Scaffold(
body: SingleChildScrollView(
child: Container(
color: Colors.amber[500],
height: 5000.0,
child: const Text('body'),
),
),
persistentFooterButtons: const <Widget>[Placeholder()],
),
),
),
);
expect(tester.getBottomLeft(find.byType(ButtonBar)), const Offset(10.0, 560.0));
expect(tester.getBottomRight(find.byType(ButtonBar)), const Offset(770.0, 560.0));
});
group('back arrow', () {
Future<void> expectBackIcon(WidgetTester tester, TargetPlatform platform, IconData expectedIcon) async {
final GlobalKey rootKey = GlobalKey();
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (_) => Container(key: rootKey, child: const Text('Home')),
'/scaffold': (_) => Scaffold(
appBar: AppBar(),
body: const Text('Scaffold'),
),
};
await tester.pumpWidget(
MaterialApp(theme: ThemeData(platform: platform), routes: routes)
);
Navigator.pushNamed(rootKey.currentContext, '/scaffold');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final Icon icon = tester.widget(find.byType(Icon));
expect(icon.icon, expectedIcon);
}
testWidgets('Back arrow uses correct default on Android', (WidgetTester tester) async {
await expectBackIcon(tester, TargetPlatform.android, Icons.arrow_back);
});
testWidgets('Back arrow uses correct default on Fuchsia', (WidgetTester tester) async {
await expectBackIcon(tester, TargetPlatform.fuchsia, Icons.arrow_back);
});
testWidgets('Back arrow uses correct default on iOS', (WidgetTester tester) async {
await expectBackIcon(tester, TargetPlatform.iOS, Icons.arrow_back_ios);
});
});
group('close button', () {
Future<void> expectCloseIcon(WidgetTester tester, TargetPlatform platform, IconData expectedIcon, PageRoute<void> routeBuilder()) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(appBar: AppBar(), body: const Text('Page 1')),
)
);
tester.state<NavigatorState>(find.byType(Navigator)).push(routeBuilder());
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final Icon icon = tester.widget(find.byType(Icon));
expect(icon.icon, expectedIcon);
expect(find.byType(CloseButton), findsOneWidget);
}
PageRoute<void> materialRouteBuilder() {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(appBar: AppBar(), body: const Text('Page 2'));
},
fullscreenDialog: true,
);
}
PageRoute<void> customPageRouteBuilder() {
return _CustomPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(appBar: AppBar(), body: const Text('Page 2'));
},
fullscreenDialog: true,
);
}
testWidgets('Close button shows correctly on Android', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.android, Icons.close, materialRouteBuilder);
});
testWidgets('Close button shows correctly on Fuchsia', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.fuchsia, Icons.close, materialRouteBuilder);
});
testWidgets('Close button shows correctly on iOS', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.iOS, Icons.close, materialRouteBuilder);
});
testWidgets('Close button shows correctly with custom page route on Android', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.android, Icons.close, customPageRouteBuilder);
});
testWidgets('Close button shows correctly with custom page route on Fuchsia', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.fuchsia, Icons.close, customPageRouteBuilder);
});
testWidgets('Close button shows correctly with custom page route on iOS', (WidgetTester tester) async {
await expectCloseIcon(tester, TargetPlatform.iOS, Icons.close, customPageRouteBuilder);
});
});
group('body size', () {
testWidgets('body size with container', (WidgetTester tester) async {
final Key testKey = UniqueKey();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Container(
key: testKey,
),
),
),
));
expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 600.0));
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
});
testWidgets('body size with sized container', (WidgetTester tester) async {
final Key testKey = UniqueKey();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Container(
key: testKey,
height: 100.0,
),
),
),
));
expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 100.0));
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
});
testWidgets('body size with centered container', (WidgetTester tester) async {
final Key testKey = UniqueKey();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Center(
child: Container(
key: testKey,
),
),
),
),
));
expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 600.0));
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
});
testWidgets('body size with button', (WidgetTester tester) async {
final Key testKey = UniqueKey();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: FlatButton(
key: testKey,
onPressed: () { },
child: const Text(''),
),
),
),
));
expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 48.0));
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
});
testWidgets('body size with extendBody', (WidgetTester tester) async {
final Key bodyKey = UniqueKey();
double mediaQueryBottom;
Widget buildFrame({ bool extendBody, bool resizeToAvoidBottomInset, double viewInsetBottom = 0.0 }) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(
viewInsets: EdgeInsets.only(bottom: viewInsetBottom),
),
child: Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
extendBody: extendBody,
body: Builder(
builder: (BuildContext context) {
mediaQueryBottom = MediaQuery.of(context).padding.bottom;
return Container(key: bodyKey);
},
),
bottomNavigationBar: const BottomAppBar(
child: SizedBox(height: 48.0,),
),
),
),
);
}
await tester.pumpWidget(buildFrame(extendBody: true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(mediaQueryBottom, 48.0);
await tester.pumpWidget(buildFrame(extendBody: false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 552.0)); // 552 = 600 - 48 (BAB height)
expect(mediaQueryBottom, 0.0);
// If resizeToAvoidBottomInsets is false, same results as if it was unspecified (null).
await tester.pumpWidget(buildFrame(extendBody: true, resizeToAvoidBottomInset: false, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
expect(mediaQueryBottom, 48.0);
await tester.pumpWidget(buildFrame(extendBody: false, resizeToAvoidBottomInset: false, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 552.0));
expect(mediaQueryBottom, 0.0);
// If resizeToAvoidBottomInsets is true and viewInsets.bottom is > the bottom
// navigation bar's height then the body always resizes and the MediaQuery
// isn't adjusted. This case corresponds to the keyboard appearing.
await tester.pumpWidget(buildFrame(extendBody: true, resizeToAvoidBottomInset: true, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
expect(mediaQueryBottom, 0.0);
await tester.pumpWidget(buildFrame(extendBody: false, resizeToAvoidBottomInset: true, viewInsetBottom: 100.0));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
expect(mediaQueryBottom, 0.0);
});
});
testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async {
const String bodyLabel = 'I am the body';
const String persistentFooterButtonLabel = 'a button on the bottom';
const String bottomNavigationBarLabel = 'a bar in an app';
const String floatingActionButtonLabel = 'I float in space';
const String drawerLabel = 'I am the reason for this test';
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(const MaterialApp(home: Scaffold(
body: Text(bodyLabel),
persistentFooterButtons: <Widget>[Text(persistentFooterButtonLabel)],
bottomNavigationBar: Text(bottomNavigationBarLabel),
floatingActionButton: Text(floatingActionButtonLabel),
drawer: Drawer(child: Text(drawerLabel)),
)));
expect(semantics, includesNodeWith(label: bodyLabel));
expect(semantics, includesNodeWith(label: persistentFooterButtonLabel));
expect(semantics, includesNodeWith(label: bottomNavigationBarLabel));
expect(semantics, includesNodeWith(label: floatingActionButtonLabel));
expect(semantics, isNot(includesNodeWith(label: drawerLabel)));
final ScaffoldState state = tester.firstState(find.byType(Scaffold));
state.openDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
expect(semantics, isNot(includesNodeWith(label: persistentFooterButtonLabel)));
expect(semantics, isNot(includesNodeWith(label: bottomNavigationBarLabel)));
expect(semantics, isNot(includesNodeWith(label: floatingActionButtonLabel)));
expect(semantics, includesNodeWith(label: drawerLabel));
semantics.dispose();
});
testWidgets('Scaffold and extreme window padding', (WidgetTester tester) async {
final Key appBar = UniqueKey();
final Key body = UniqueKey();
final Key floatingActionButton = UniqueKey();
final Key persistentFooterButton = UniqueKey();
final Key drawer = UniqueKey();
final Key bottomNavigationBar = UniqueKey();
final Key insideAppBar = UniqueKey();
final Key insideBody = UniqueKey();
final Key insideFloatingActionButton = UniqueKey();
final Key insidePersistentFooterButton = UniqueKey();
final Key insideDrawer = UniqueKey();
final Key insideBottomNavigationBar = UniqueKey();
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
left: 20.0,
top: 30.0,
right: 50.0,
bottom: 60.0,
),
viewInsets: EdgeInsets.only(bottom: 200.0),
),
child: Scaffold(
drawerDragStartBehavior: DragStartBehavior.down,
appBar: PreferredSize(
preferredSize: const Size(11.0, 13.0),
child: Container(
key: appBar,
child: SafeArea(
child: Placeholder(key: insideAppBar),
),
),
),
body: Container(
key: body,
child: SafeArea(
child: Placeholder(key: insideBody),
),
),
floatingActionButton: SizedBox(
key: floatingActionButton,
width: 77.0,
height: 77.0,
child: SafeArea(
child: Placeholder(key: insideFloatingActionButton),
),
),
persistentFooterButtons: <Widget>[
SizedBox(
key: persistentFooterButton,
width: 100.0,
height: 90.0,
child: SafeArea(
child: Placeholder(key: insidePersistentFooterButton),
),
),
],
drawer: Container(
key: drawer,
width: 204.0,
child: SafeArea(
child: Placeholder(key: insideDrawer),
),
),
bottomNavigationBar: SizedBox(
key: bottomNavigationBar,
height: 85.0,
child: SafeArea(
child: Placeholder(key: insideBottomNavigationBar),
),
),
),
),
),
),
);
// open drawer
await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.getRect(find.byKey(appBar)), Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
expect(tester.getRect(find.byKey(body)), Rect.fromLTRB(0.0, 43.0, 800.0, 348.0));
expect(tester.getRect(find.byKey(floatingActionButton)), Rect.fromLTRB(36.0, 255.0, 113.0, 332.0));
expect(tester.getRect(find.byKey(persistentFooterButton)), Rect.fromLTRB(28.0, 357.0, 128.0, 447.0)); // Note: has 8px each top/bottom padding.
expect(tester.getRect(find.byKey(drawer)), Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
expect(tester.getRect(find.byKey(bottomNavigationBar)), Rect.fromLTRB(0.0, 515.0, 800.0, 600.0));
expect(tester.getRect(find.byKey(insideAppBar)), Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
expect(tester.getRect(find.byKey(insideBody)), Rect.fromLTRB(20.0, 43.0, 750.0, 348.0));
expect(tester.getRect(find.byKey(insideFloatingActionButton)), Rect.fromLTRB(36.0, 255.0, 113.0, 332.0));
expect(tester.getRect(find.byKey(insidePersistentFooterButton)), Rect.fromLTRB(28.0, 357.0, 128.0, 447.0));
expect(tester.getRect(find.byKey(insideDrawer)), Rect.fromLTRB(596.0, 30.0, 750.0, 540.0));
expect(tester.getRect(find.byKey(insideBottomNavigationBar)), Rect.fromLTRB(20.0, 515.0, 750.0, 540.0));
});
testWidgets('Scaffold and extreme window padding - persistent footer buttons only', (WidgetTester tester) async {
final Key appBar = UniqueKey();
final Key body = UniqueKey();
final Key floatingActionButton = UniqueKey();
final Key persistentFooterButton = UniqueKey();
final Key drawer = UniqueKey();
final Key insideAppBar = UniqueKey();
final Key insideBody = UniqueKey();
final Key insideFloatingActionButton = UniqueKey();
final Key insidePersistentFooterButton = UniqueKey();
final Key insideDrawer = UniqueKey();
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
left: 20.0,
top: 30.0,
right: 50.0,
bottom: 60.0,
),
viewInsets: EdgeInsets.only(bottom: 200.0),
),
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size(11.0, 13.0),
child: Container(
key: appBar,
child: SafeArea(
child: Placeholder(key: insideAppBar),
),
),
),
body: Container(
key: body,
child: SafeArea(
child: Placeholder(key: insideBody),
),
),
floatingActionButton: SizedBox(
key: floatingActionButton,
width: 77.0,
height: 77.0,
child: SafeArea(
child: Placeholder(key: insideFloatingActionButton),
),
),
persistentFooterButtons: <Widget>[
SizedBox(
key: persistentFooterButton,
width: 100.0,
height: 90.0,
child: SafeArea(
child: Placeholder(key: insidePersistentFooterButton),
),
),
],
drawer: Container(
key: drawer,
width: 204.0,
child: SafeArea(
child: Placeholder(key: insideDrawer),
),
),
),
),
),
),
);
// open drawer
await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.getRect(find.byKey(appBar)), Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
expect(tester.getRect(find.byKey(body)), Rect.fromLTRB(0.0, 43.0, 800.0, 400.0));
expect(tester.getRect(find.byKey(floatingActionButton)), Rect.fromLTRB(36.0, 307.0, 113.0, 384.0));
expect(tester.getRect(find.byKey(persistentFooterButton)), Rect.fromLTRB(28.0, 442.0, 128.0, 532.0)); // Note: has 8px each top/bottom padding.
expect(tester.getRect(find.byKey(drawer)), Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
expect(tester.getRect(find.byKey(insideAppBar)), Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
expect(tester.getRect(find.byKey(insideBody)), Rect.fromLTRB(20.0, 43.0, 750.0, 400.0));
expect(tester.getRect(find.byKey(insideFloatingActionButton)), Rect.fromLTRB(36.0, 307.0, 113.0, 384.0));
expect(tester.getRect(find.byKey(insidePersistentFooterButton)), Rect.fromLTRB(28.0, 442.0, 128.0, 532.0));
expect(tester.getRect(find.byKey(insideDrawer)), Rect.fromLTRB(596.0, 30.0, 750.0, 540.0));
});
group('ScaffoldGeometry', () {
testWidgets('bottomNavigationBar', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: Container(),
bottomNavigationBar: ConstrainedBox(
key: key,
constraints: const BoxConstraints.expand(height: 80.0),
child: _GeometryListener(),
),
)));
final RenderBox navigationBox = tester.renderObject(find.byKey(key));
final RenderBox appBox = tester.renderObject(find.byType(MaterialApp));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
geometry.bottomNavigationBarTop,
appBox.size.height - navigationBox.size.height,
);
});
testWidgets('no bottomNavigationBar', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: _GeometryListener(),
),
)));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
geometry.bottomNavigationBarTop,
null,
);
});
testWidgets('floatingActionButton', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
key: key,
child: _GeometryListener(),
onPressed: () { },
),
)));
final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;
expect(
geometry.floatingActionButtonArea,
fabRect,
);
});
testWidgets('no floatingActionButton', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: _GeometryListener(),
),
)));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
geometry.floatingActionButtonArea,
null,
);
});
testWidgets('floatingActionButton entrance/exit animation', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: _GeometryListener(),
),
)));
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
key: key,
child: _GeometryListener(),
onPressed: () { },
),
)));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
await tester.pump(const Duration(milliseconds: 50));
ScaffoldGeometry geometry = listenerState.cache.value;
final Rect transitioningFabRect = geometry.floatingActionButtonArea;
final double transitioningRotation = tester.widget<RotationTransition>(
find.byType(RotationTransition),
).turns.value;
await tester.pump(const Duration(seconds: 3));
geometry = listenerState.cache.value;
final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;
final double completedRotation = tester.widget<RotationTransition>(
find.byType(RotationTransition),
).turns.value;
expect(transitioningRotation, lessThan(1.0));
expect(completedRotation, equals(1.0));
expect(
geometry.floatingActionButtonArea,
fabRect,
);
expect(
geometry.floatingActionButtonArea.center,
transitioningFabRect.center,
);
expect(
geometry.floatingActionButtonArea.width,
greaterThan(transitioningFabRect.width),
);
expect(
geometry.floatingActionButtonArea.height,
greaterThan(transitioningFabRect.height),
);
});
testWidgets('change notifications', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
int numNotificationsAtLastFrame = 0;
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: _GeometryListener(),
),
)));
final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
await tester.pumpWidget(MaterialApp(home: Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
key: key,
child: _GeometryListener(),
onPressed: () { },
),
)));
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
await tester.pump(const Duration(milliseconds: 50));
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
await tester.pump(const Duration(seconds: 3));
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
});
testWidgets('Simultaneous drawers on either side', (WidgetTester tester) async {
const String bodyLabel = 'I am the body';
const String drawerLabel = 'I am the label on start side';
const String endDrawerLabel = 'I am the label on end side';
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(const MaterialApp(home: Scaffold(
body: Text(bodyLabel),
drawer: Drawer(child: Text(drawerLabel)),
endDrawer: Drawer(child: Text(endDrawerLabel)),
)));
expect(semantics, includesNodeWith(label: bodyLabel));
expect(semantics, isNot(includesNodeWith(label: drawerLabel)));
expect(semantics, isNot(includesNodeWith(label: endDrawerLabel)));
final ScaffoldState state = tester.firstState(find.byType(Scaffold));
state.openDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
expect(semantics, includesNodeWith(label: drawerLabel));
state.openEndDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
expect(semantics, includesNodeWith(label: endDrawerLabel));
semantics.dispose();
});
testWidgets('Drawer state query correctly', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: SafeArea(
left: false,
top: true,
right: false,
bottom: false,
child: Scaffold(
endDrawer: const Drawer(
child: Text('endDrawer'),
),
drawer: const Drawer(
child: Text('drawer'),
),
body: const Text('scaffold body'),
appBar: AppBar(
centerTitle: true,
title: const Text('Title'),
),
),
),
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
final Finder drawerOpenButton = find.byType(IconButton).first;
final Finder endDrawerOpenButton = find.byType(IconButton).last;
await tester.tap(drawerOpenButton);
await tester.pumpAndSettle();
expect(true, scaffoldState.isDrawerOpen);
await tester.tap(endDrawerOpenButton);
await tester.pumpAndSettle();
expect(false, scaffoldState.isDrawerOpen);
await tester.tap(endDrawerOpenButton);
await tester.pumpAndSettle();
expect(true, scaffoldState.isEndDrawerOpen);
await tester.tap(drawerOpenButton);
await tester.pumpAndSettle();
expect(false, scaffoldState.isEndDrawerOpen);
scaffoldState.openDrawer();
expect(true, scaffoldState.isDrawerOpen);
await tester.tap(drawerOpenButton);
await tester.pumpAndSettle();
scaffoldState.openEndDrawer();
expect(true, scaffoldState.isEndDrawerOpen);
});
testWidgets('Dual Drawer Opening', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: SafeArea(
left: false,
top: true,
right: false,
bottom: false,
child: Scaffold(
endDrawer: const Drawer(
child: Text('endDrawer'),
),
drawer: const Drawer(
child: Text('drawer'),
),
body: const Text('scaffold body'),
appBar: AppBar(
centerTitle: true,
title: const Text('Title'),
),
),
),
),
);
// Open Drawer, tap on end drawer, which closes the drawer, but does
// not open the drawer.
await tester.tap(find.byType(IconButton).first);
await tester.pumpAndSettle();
await tester.tap(find.byType(IconButton).last);
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsNothing);
expect(find.text('drawer'), findsNothing);
// Tapping the first opens the first drawer
await tester.tap(find.byType(IconButton).first);
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsNothing);
expect(find.text('drawer'), findsOneWidget);
// Tapping on the end drawer and then on the drawer should close the
// drawer and then reopen it.
await tester.tap(find.byType(IconButton).last);
await tester.pumpAndSettle();
await tester.tap(find.byType(IconButton).first);
await tester.pumpAndSettle();
expect(find.text('endDrawer'), findsNothing);
expect(find.text('drawer'), findsOneWidget);
});
testWidgets('Drawer opens correctly with padding from MediaQuery', (WidgetTester tester) async {
// The padding described by MediaQuery is larger than the default
// drawer drag zone width which is 20.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
drawer: const Drawer(
child: Text('drawer'),
),
body: const Text('scaffold body'),
appBar: AppBar(
centerTitle: true,
title: const Text('Title'),
),
),
),
);
ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
expect(scaffoldState.isDrawerOpen, false);
await tester.dragFrom(const Offset(35, 100), const Offset(300, 0));
await tester.pumpAndSettle();
expect(scaffoldState.isDrawerOpen, false);
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(40, 0, 0, 0)
),
child: Scaffold(
drawer: const Drawer(
child: Text('drawer'),
),
body: const Text('scaffold body'),
appBar: AppBar(
centerTitle: true,
title: const Text('Title'),
),
),
),
),
);
scaffoldState = tester.state(find.byType(Scaffold));
expect(scaffoldState.isDrawerOpen, false);
await tester.dragFrom(const Offset(35, 100), const Offset(300, 0));
await tester.pumpAndSettle();
expect(scaffoldState.isDrawerOpen, true);
});
testWidgets('Drawer opens correctly with padding from MediaQuer (RTL)', (WidgetTester tester) async {
// The padding described by MediaQuery is larger than the default
// drawer drag zone width which is 20.
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(0, 0, 40, 0)
),
child: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
drawer: const Drawer(
child: Text('drawer'),
),
body: const Text('scaffold body'),
appBar: AppBar(
centerTitle: true,
title: const Text('Title'),
),
),
),
),
),
);
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
expect(scaffoldState.isDrawerOpen, false);
await tester.dragFrom(const Offset(765, 100), const Offset(-300, 0));
await tester.pumpAndSettle();
expect(scaffoldState.isDrawerOpen, true);
});
});
testWidgets('Nested scaffold body insets', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/20295
final Key bodyKey = UniqueKey();
Widget buildFrame(bool innerResizeToAvoidBottomInset, bool outerResizeToAvoidBottomInset) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)),
child: Builder(
builder: (BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: outerResizeToAvoidBottomInset,
body: Builder(
builder: (BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: innerResizeToAvoidBottomInset,
body: Container(key: bodyKey),
);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame(true, true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
await tester.pumpWidget(buildFrame(false, true));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
await tester.pumpWidget(buildFrame(true, false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
// This is the only case where the body is not bottom inset.
await tester.pumpWidget(buildFrame(false, false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 600.0));
await tester.pumpWidget(buildFrame(null, null)); // resizeToAvoidBottomInset default is true
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
await tester.pumpWidget(buildFrame(null, false));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
await tester.pumpWidget(buildFrame(false, null));
expect(tester.getSize(find.byKey(bodyKey)), const Size(800.0, 500.0));
});
}
class _GeometryListener extends StatefulWidget {
@override
_GeometryListenerState createState() => _GeometryListenerState();
}
class _GeometryListenerState extends State<_GeometryListener> {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: cache
);
}
int numNotifications = 0;
ValueListenable<ScaffoldGeometry> geometryListenable;
_GeometryCachePainter cache;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
if (geometryListenable != null)
geometryListenable.removeListener(onGeometryChanged);
geometryListenable = newListenable;
geometryListenable.addListener(onGeometryChanged);
cache = _GeometryCachePainter(geometryListenable);
}
void onGeometryChanged() {
numNotifications += 1;
}
}
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class _GeometryCachePainter extends CustomPainter {
_GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
ScaffoldGeometry value;
@override
void paint(Canvas canvas, Size size) {
value = geometryListenable.value;
}
@override
bool shouldRepaint(_GeometryCachePainter oldDelegate) {
return true;
}
}
class _CustomPageRoute<T> extends PageRoute<T> {
_CustomPageRoute({
@required this.builder,
RouteSettings settings = const RouteSettings(),
this.maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: fullscreenDialog);
final WidgetBuilder builder;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
final bool maintainState;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return child;
}
}