blob: 8b90468dddef66a260824394551e18420e005619 [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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation drawer updates destinations when tapped', (WidgetTester tester) async {
var mutatedIndex = -1;
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
widgetSetup(tester, 3000, viewHeight: 3000);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {
mutatedIndex = i;
},
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState!.openDrawer();
await tester.pump();
expect(find.text('Headline'), findsOneWidget);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
await tester.pump(const Duration(seconds: 1)); // animation done
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
await tester.tap(find.text('AC'));
expect(mutatedIndex, 0);
});
testWidgets('NavigationDrawer can update background color', (WidgetTester tester) async {
const Color color = Colors.yellow;
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
backgroundColor: color,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {},
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1)); // animation done
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('NavigationDestinationDrawer background color is customizable', (
WidgetTester tester,
) async {
const Color color = Colors.yellow;
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm', style: theme.textTheme.bodySmall),
backgroundColor: color,
),
],
onDestinationSelected: (int i) {},
),
),
);
Finder findDestinationInk(String label) {
return find.descendant(
of: find.ancestor(of: find.text(label), matching: find.byType(NavigationDrawerDestination)),
matching: find.byType(Ink),
);
}
scaffoldKey.currentState!.openDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // Animation done.
// Destination with no custom background color.
await tester.tap(find.text('AC'));
await tester.pump();
// When no background color is set, only the non-visible indicator Ink is expected.
expect(findDestinationInk('AC'), findsOne);
// Destination with a custom background color.
await tester.tap(find.byIcon(Icons.access_alarm));
await tester.pump();
// A Material is added with the custom color.
expect(findDestinationInk('Alarm'), findsNWidgets(2));
final destinationDecoration =
tester.firstWidget<Ink>(findDestinationInk('Alarm')).decoration! as BoxDecoration;
expect(destinationDecoration.color, color);
});
testWidgets('NavigationDrawer can update elevation', (WidgetTester tester) async {
const elevation = 42.0;
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
final drawer = NavigationDrawer(
elevation: elevation,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
);
await tester.pumpWidget(_buildWidget(scaffoldKey, drawer));
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(_getMaterial(tester).elevation, equals(elevation));
});
testWidgets('NavigationDrawer uses proper defaults when no parameters are given', (
WidgetTester tester,
) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
const NavigationDrawerDestination(icon: Icon(Icons.ac_unit), label: Text('AC')),
const NavigationDrawerDestination(icon: Icon(Icons.access_alarm), label: Text('Alarm')),
],
onDestinationSelected: (int i) {},
),
useMaterial3: theme.useMaterial3,
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
// Test drawer Material.
expect(_getMaterial(tester).color, theme.colorScheme.surfaceContainerLow);
expect(_getMaterial(tester).surfaceTintColor, Colors.transparent);
expect(_getMaterial(tester).shadowColor, Colors.transparent);
expect(_getMaterial(tester).elevation, 1);
// Test indicator decoration.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
// Test selected and unselected icon colors.
expect(_iconStyle(tester, Icons.ac_unit)?.color, theme.colorScheme.onSecondaryContainer);
expect(_iconStyle(tester, Icons.access_alarm)?.color, theme.colorScheme.onSurfaceVariant);
// Test selected and unselected label colors.
expect(_labelStyle(tester, 'AC')?.color, theme.colorScheme.onSecondaryContainer);
expect(_labelStyle(tester, 'Alarm')?.color, theme.colorScheme.onSurfaceVariant);
// Test that the icon and label are the correct size.
RenderBox iconBox = tester.renderObject(find.byIcon(Icons.ac_unit));
expect(iconBox.size, const Size(24.0, 24.0));
iconBox = tester.renderObject(find.byIcon(Icons.access_alarm));
expect(iconBox.size, const Size(24.0, 24.0));
});
testWidgets('Navigation drawer is scrollable', (WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
widgetSetup(tester, 500, viewHeight: 300);
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
children: <Widget>[
for (int i = 0; i < 100; i++)
NavigationDrawerDestination(icon: const Icon(Icons.ac_unit), label: Text('Label$i')),
],
onDestinationSelected: (int i) {},
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Label0'), findsOneWidget);
expect(find.text('Label1'), findsOneWidget);
expect(find.text('Label2'), findsOneWidget);
expect(find.text('Label3'), findsOneWidget);
expect(find.text('Label4'), findsOneWidget);
expect(find.text('Label5'), findsOneWidget);
expect(find.text('Label6'), findsNothing);
expect(find.text('Label7'), findsNothing);
expect(find.text('Label8'), findsNothing);
await tester.dragFrom(const Offset(0, 200), const Offset(0.0, -200));
await tester.pump();
expect(find.text('Label0'), findsNothing);
expect(find.text('Label1'), findsNothing);
expect(find.text('Label2'), findsNothing);
expect(find.text('Label3'), findsOneWidget);
expect(find.text('Label4'), findsOneWidget);
expect(find.text('Label5'), findsOneWidget);
expect(find.text('Label6'), findsOneWidget);
expect(find.text('Label7'), findsOneWidget);
expect(find.text('Label8'), findsOneWidget);
expect(find.text('Label9'), findsNothing);
expect(find.text('Label10'), findsNothing);
});
testWidgets('Safe Area test', (WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
const double viewHeight = 300;
widgetSetup(tester, 500, viewHeight: viewHeight);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.all(20.0)),
child: MaterialApp(
useInheritedMediaQuery: true,
home: Scaffold(
key: scaffoldKey,
drawer: NavigationDrawer(
children: <Widget>[
for (int i = 0; i < 10; i++)
NavigationDrawerDestination(
icon: const Icon(Icons.ac_unit),
label: Text('Label$i'),
),
],
onDestinationSelected: (int i) {},
),
body: Container(),
),
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Safe area padding on the top and sides.
expect(
tester.getTopLeft(find.widgetWithText(NavigationDrawerDestination, 'Label0')),
const Offset(20.0, 20.0),
);
// No Safe area padding at the bottom.
expect(
tester.getBottomRight(find.widgetWithText(NavigationDrawerDestination, 'Label4')).dy,
viewHeight,
);
});
testWidgets('Navigation drawer semantics', (WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
Widget widget({int selectedIndex = 0}) {
return _buildWidget(
scaffoldKey,
NavigationDrawer(
selectedIndex: selectedIndex,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit, color: theme.iconTheme.color),
label: Text('AC', style: theme.textTheme.bodySmall),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
),
);
}
await tester.pumpWidget(widget());
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasSelectedState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
hasSelectedState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
await tester.pumpWidget(widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
hasSelectedState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasSelectedState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
testWidgets('Navigation destination updates indicator color and shape', (
WidgetTester tester,
) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
final theme = ThemeData();
const color = Color(0xff0000ff);
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNavigationDrawer({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
key: scaffoldKey,
drawer: NavigationDrawer(
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
const NavigationDrawerDestination(icon: Icon(Icons.ac_unit), label: Text('AC')),
const NavigationDrawerDestination(
icon: Icon(Icons.access_alarm),
label: Text('Alarm'),
),
],
onDestinationSelected: (int i) {},
),
body: Container(),
),
);
}
await tester.pumpWidget(buildNavigationDrawer());
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
// Test default indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
// Test that InkWell for hover, focus and pressed use default shape.
expect(_getInkWell(tester)?.customBorder, const StadiumBorder());
await tester.pumpWidget(buildNavigationDrawer(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
// Test that InkWell for hover, focus and pressed use custom shape.
expect(_getInkWell(tester)?.customBorder, shape);
});
testWidgets('NavigationDrawer.tilePadding defaults to EdgeInsets.symmetric(horizontal: 12.0)', (
WidgetTester tester,
) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
widgetSetup(tester, 3000, viewHeight: 3000);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
children: const <Widget>[
NavigationDrawerDestination(icon: Icon(Icons.ac_unit), label: Text('AC')),
],
onDestinationSelected: (int i) {},
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState?.openDrawer();
await tester.pump();
final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer));
expect(drawer.tilePadding, const EdgeInsets.symmetric(horizontal: 12.0));
});
testWidgets('Destinations respect their disabled state', (WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
var selectedIndex = 0;
widgetSetup(tester, 800);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
children: const <Widget>[
NavigationDrawerDestination(icon: Icon(Icons.ac_unit), label: Text('AC')),
NavigationDrawerDestination(icon: Icon(Icons.access_alarm), label: Text('Alarm')),
NavigationDrawerDestination(
icon: Icon(Icons.accessible),
label: Text('Accessible'),
enabled: false,
),
],
onDestinationSelected: (int i) {
selectedIndex = i;
},
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState!.openDrawer();
await tester.pump();
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
expect(find.text('Accessible'), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
expect(selectedIndex, 0);
await tester.tap(find.text('Alarm'));
expect(selectedIndex, 1);
await tester.tap(find.text('Accessible'));
expect(selectedIndex, 1);
await tester.pumpAndSettle();
});
testWidgets('NavigationDrawer can display header and footer', (WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
widgetSetup(tester, 3000, viewHeight: 3000);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
header: const DrawerHeader(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: <Widget>[FlutterLogo(), Text('Header')],
),
),
),
footer: ListTile(
leading: const FlutterLogo(),
title: const Text('Footer'),
trailing: const Icon(Icons.settings),
onTap: () {},
),
children: <Widget>[
for (int i = 0; i < 10; i++)
NavigationDrawerDestination(icon: const Icon(Icons.home), label: Text('Item $i')),
],
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState!.openDrawer();
await tester.pump(const Duration(seconds: 1));
expect(find.byType(DrawerHeader), findsOneWidget);
expect(find.text('Header'), findsOneWidget);
expect(find.byType(FlutterLogo), findsNWidgets(2));
expect(find.byType(ListTile), findsOneWidget);
expect(find.text('Footer'), findsOneWidget);
expect(find.byIcon(Icons.settings), findsOneWidget);
});
testWidgets('NavigationDrawer does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.shrink(
child: NavigationDrawer(
children: <Widget>[
NavigationDrawerDestination(icon: Icon(Icons.inbox), label: Text('Inbox')),
],
),
),
),
),
);
expect(tester.getSize(find.byType(NavigationDrawer)), Size.zero);
});
// Regression test for https://github.com/flutter/flutter/issues/180233
testWidgets(
'NavigationDrawer ink effects are bounded within scrollable area when footer is present',
(WidgetTester tester) async {
final scaffoldKey = GlobalKey<ScaffoldState>();
widgetSetup(tester, 800, viewHeight: 400);
await tester.pumpWidget(
_buildWidget(
scaffoldKey,
NavigationDrawer(
footer: const Padding(padding: EdgeInsets.all(16.0), child: Text('Footer')),
children: <Widget>[
for (int i = 0; i < 10; i++)
NavigationDrawerDestination(icon: const Icon(Icons.home), label: Text('Item $i')),
],
),
),
);
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
final Finder footerFinder = find
.ancestor(of: find.text('Footer'), matching: find.byType(Padding))
.first;
expect(footerFinder, findsOneWidget);
final Rect footerRect = tester.getRect(footerFinder);
final Finder inkWellFinder = find.descendant(
of: find.byType(NavigationDrawerDestination).first,
matching: find.byType(InkWell),
);
expect(inkWellFinder, findsOneWidget);
final inkMaterial = Material.of(tester.element(inkWellFinder)) as RenderBox;
final Offset inkMaterialTopLeft = inkMaterial.localToGlobal(Offset.zero);
final Offset inkMaterialBottomRight = inkMaterial.localToGlobal(
Offset(inkMaterial.size.width, inkMaterial.size.height),
);
final inkMaterialRect = Rect.fromPoints(inkMaterialTopLeft, inkMaterialBottomRight);
expect(inkMaterialRect.bottom, equals(footerRect.top));
},
);
}
Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, {bool? useMaterial3}) {
return MaterialApp(
theme: ThemeData(useMaterial3: useMaterial3),
home: Scaffold(key: scaffoldKey, drawer: child, body: Container()),
);
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(of: find.byType(NavigationDrawer), matching: find.byType(Material)),
);
}
InkWell? _getInkWell(WidgetTester tester) {
return tester.firstWidget<InkWell>(
find.descendant(of: find.byType(NavigationDrawer), matching: find.byType(InkWell)),
);
}
ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
return tester
.firstWidget<Ink>(
find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Ink)),
)
.decoration
as ShapeDecoration?;
}
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
TextStyle? _labelStyle(WidgetTester tester, String label) {
final RichText labelRichText = tester.widget<RichText>(
find.descendant(of: find.text(label), matching: find.byType(RichText)),
);
return labelRichText.text.style;
}
void widgetSetup(WidgetTester tester, double viewWidth, {double viewHeight = 1000}) {
tester.view.devicePixelRatio = 2;
final double dpi = tester.view.devicePixelRatio;
tester.view.physicalSize = Size(viewWidth * dpi, viewHeight * dpi);
addTearDown(tester.view.reset);
}