blob: 43046c2fefc37e390704cd0eccfc016f3d5f38eb [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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
int mutatedIndex = -1;
final Widget widget = _buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {
mutatedIndex = i;
},
),
);
await tester.pumpWidget(widget);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
await tester.tap(find.text('AC'));
expect(mutatedIndex, 0);
});
testWidgets('NavigationBar can update background color', (WidgetTester tester) async {
const Color color = Colors.yellow;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
backgroundColor: color,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('NavigationBar can update elevation', (WidgetTester tester) async {
const double elevation = 42.0;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
elevation: elevation,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).elevation, equals(elevation));
});
testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
const double bottomPadding = 40.0;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
final double defaultSize = tester.getSize(find.byType(NavigationBar)).height;
expect(defaultSize, 80);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
),
);
final double expectedHeight = defaultSize + bottomPadding;
expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
});
testWidgets('NavigationBar respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async {
const double safeAreaPadding = 40.0;
Widget navigationBar() {
return NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
key: Key('Center'),
icon: Icon(Icons.center_focus_strong),
label: 'Center',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
);
}
await tester.pumpWidget(_buildWidget(navigationBar()));
final double defaultWidth = tester.getSize(find.byType(NavigationBar)).width;
final Finder defaultCenterItem = find.byKey(const Key('Center'));
final Offset center = tester.getCenter(defaultCenterItem);
expect(center.dx, defaultWidth / 2);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(left: safeAreaPadding),
),
child: navigationBar(),
),
),
);
// The position of center item of navigation bar should indicate whether
// the safe area is sufficiently respected, when safe area is on the left side.
// e.g. Android device with system navigation bar in landscape mode.
final Finder leftPaddedCenterItem = find.byKey(const Key('Center'));
final Offset leftPaddedCenter = tester.getCenter(leftPaddedCenterItem);
expect(
leftPaddedCenter.dx,
closeTo((defaultWidth + safeAreaPadding) / 2.0, precisionErrorTolerance),
);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(right: safeAreaPadding)
),
child: navigationBar(),
),
),
);
// The position of center item of navigation bar should indicate whether
// the safe area is sufficiently respected, when safe area is on the right side.
// e.g. Android device with system navigation bar in landscape mode.
final Finder rightPaddedCenterItem = find.byKey(const Key('Center'));
final Offset rightPaddedCenter = tester.getCenter(rightPaddedCenterItem);
expect(
rightPaddedCenter.dx,
closeTo((defaultWidth - safeAreaPadding) / 2, precisionErrorTolerance),
);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(
safeAreaPadding,
0,
safeAreaPadding,
safeAreaPadding
),
),
child: navigationBar(),
),
),
);
// The position of center item of navigation bar should indicate whether
// the safe area is sufficiently respected, when safe areas are on both sides.
// e.g. iOS device with both sides of round corner.
final Finder paddedCenterItem = find.byKey(const Key('Center'));
final Offset paddedCenter = tester.getCenter(paddedCenterItem);
expect(
paddedCenter.dx,
closeTo(defaultWidth / 2, precisionErrorTolerance),
);
});
testWidgets('NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async {
// Pre-M3 settings that were hand coded.
await tester.pumpWidget(
_buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).color, const Color(0xffeaeaea));
expect(_getMaterial(tester).elevation, 0);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_indicator(tester)?.color, const Color(0x3d2196f3));
expect(_indicator(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)));
// M3 settings from the token database.
await tester.pumpWidget(
_buildWidget(
Theme(
data: ThemeData.light().copyWith(useMaterial3: true),
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
),
);
expect(_getMaterial(tester).color, const Color(0xffecf6fe));
expect(_getMaterial(tester).elevation, 3);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_indicator(tester)?.color, const Color(0xff2196f3));
expect(_indicator(tester)?.shape, const StadiumBorder());
});
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
const String label = 'A';
Widget buildApp({ required double textScaleFactor }) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: label,
icon: Icon(Icons.ac_unit),
tooltip: label,
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(find.text(label), findsNWidgets(2));
// The default size of a tooltip with the text A.
const Size defaultTooltipSize = Size(14.0, 14.0);
expect(tester.getSize(find.text(label).last), defaultTooltipSize);
// The duration is needed to ensure the tooltip disappears.
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(tester.getSize(find.text(label).last), Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4));
});
testWidgets('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: 'A',
tooltip: 'A tooltip',
icon: Icon(Icons.ac_unit),
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
NavigationDestination(
label: 'C',
icon: Icon(Icons.cake),
tooltip: '',
),
],
),
),
),
);
expect(find.text('A'), findsOneWidget);
await tester.longPress(find.text('A'));
expect(find.byTooltip('A tooltip'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
await tester.longPress(find.text('B'));
expect(find.byTooltip('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
await tester.longPress(find.text('C'));
expect(find.byTooltip('C'), findsNothing);
});
testWidgets('Navigation bar semantics', (WidgetTester tester) async {
Widget widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(widget());
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: 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,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar semantics with some labels hidden', (WidgetTester tester) async {
Widget widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(widget());
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: 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,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
const int animationMilliseconds = 800;
Widget widget({double textScaleFactor = 1}) {
return _buildWidget(
MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: NavigationBar(
animationDuration: const Duration(milliseconds: animationMilliseconds),
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
),
);
}
await tester.pumpWidget(widget());
final double initialHeight = tester.getSize(find.byType(NavigationBar)).height;
await tester.pumpWidget(widget(textScaleFactor: 2));
final double newHeight = tester.getSize(find.byType(NavigationBar)).height;
expect(newHeight, equals(initialHeight));
});
}
Widget _buildWidget(Widget child) {
return MaterialApp(
theme: ThemeData.light(),
home: Scaffold(
bottomNavigationBar: Center(
child: child,
),
),
);
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
);
}
ShapeDecoration? _indicator(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as ShapeDecoration?;
}