blob: b3a6b17ccdbb957dc5b3d1a4b36a2d57db122b3b [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.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
var 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 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 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 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('Material2 - NavigationBar uses proper defaults when no parameters are given', (
WidgetTester tester,
) async {
// M2 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) {},
),
useMaterial3: false,
),
);
expect(_getMaterial(tester).color, const Color(0xffeaeaea));
expect(_getMaterial(tester).surfaceTintColor, null);
expect(_getMaterial(tester).elevation, 0);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_getIndicatorDecoration(tester)?.color, const Color(0x3d2196f3));
expect(
_getIndicatorDecoration(tester)?.shape,
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
);
});
testWidgets('Material3 - NavigationBar uses proper defaults when no parameters are given', (
WidgetTester tester,
) async {
// M3 settings from the token database.
final theme = ThemeData();
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) {},
),
useMaterial3: theme.useMaterial3,
),
);
expect(_getMaterial(tester).color, theme.colorScheme.surfaceContainer);
expect(_getMaterial(tester).surfaceTintColor, Colors.transparent);
expect(_getMaterial(tester).elevation, 3);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
});
testWidgets('Material2 - NavigationBar shows tooltips with text scaling', (
WidgetTester tester,
) async {
const label = 'A';
Widget buildApp({required TextScaler textScaler}) {
return MediaQuery(
data: MediaQueryData(textScaler: textScaler),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MaterialApp(
theme: ThemeData(useMaterial3: false),
home: 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(textScaler: TextScaler.noScaling));
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 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(textScaler: const TextScaler.linear(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('Material3 - NavigationBar shows tooltips with text scaling', (
WidgetTester tester,
) async {
const label = 'A';
Widget buildApp({required TextScaler textScaler}) {
return MediaQuery(
data: MediaQueryData(textScaler: textScaler),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MaterialApp(
home: 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(textScaler: TextScaler.noScaling));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(find.text(label), findsNWidgets(2));
expect(tester.getSize(find.text(label).last), const Size(14.25, 20.0));
// The duration is needed to ensure the tooltip disappears.
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(4.0)));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(tester.getSize(find.text(label).last), const Size(56.25, 80.0));
});
testWidgets('Material3 - NavigationBar label can scale and has maxScaleFactor', (
WidgetTester tester,
) async {
const label = 'A';
Widget buildApp({required TextScaler textScaler}) {
return MediaQuery(
data: MediaQueryData(textScaler: textScaler),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MaterialApp(
home: 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)),
NavigationDestination(label: 'B', icon: Icon(Icons.battery_alert)),
],
),
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp(textScaler: TextScaler.noScaling));
expect(find.text(label), findsOneWidget);
expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(12.5, 16.0)), true);
await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(1.1)));
await tester.pumpAndSettle();
expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(13.7, 18.0)), true);
await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(1.3)));
expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(16.1, 21.0)), true);
await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(4)));
expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(16.1, 21.0)), true);
});
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'),
NavigationDestination(icon: Icon(Icons.abc), label: 'ABC'),
],
),
);
}
await tester.pumpWidget(widget());
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC${kIsWeb ? '' : '\nTab 1 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
isButton: true,
hasSelectedState: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasSelectedState: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('ABC')),
matchesSemantics(
label: 'ABC${kIsWeb ? '' : '\nTab 3 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasSelectedState: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
await tester.pumpWidget(widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC${kIsWeb ? '' : '\nTab 1 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('ABC')),
matchesSemantics(
label: 'ABC${kIsWeb ? '' : '\nTab 3 of 3'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
testWidgets('Navigation bar disabled 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', enabled: false),
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'Another'),
],
),
);
}
await tester.pumpWidget(widget());
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC${kIsWeb ? '' : '\nTab 1 of 2'}',
textDirection: TextDirection.ltr,
isSelected: true,
hasSelectedState: true,
hasEnabledState: true,
isButton: 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${kIsWeb ? '' : '\nTab 1 of 2'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 2'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
await tester.pumpWidget(widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC${kIsWeb ? '' : '\nTab 1 of 2'}',
textDirection: TextDirection.ltr,
isFocusable: true,
isButton: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 2'}',
textDirection: TextDirection.ltr,
isFocusable: true,
hasEnabledState: true,
hasSelectedState: true,
isEnabled: true,
isSelected: true,
isButton: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
const animationMilliseconds = 800;
Widget widget({TextScaler textScaler = TextScaler.noScaling}) {
return _buildWidget(
MediaQuery(
data: MediaQueryData(textScaler: textScaler),
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(textScaler: const TextScaler.linear(2)));
final double newHeight = tester.getSize(find.byType(NavigationBar)).height;
expect(newHeight, equals(initialHeight));
});
testWidgets('Material3 - Navigation indicator renders ripple', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/116751.
var selectedIndex = 0;
Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: Center(
child: NavigationBar(
selectedIndex: selectedIndex,
labelBehavior: labelBehavior,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildWidget());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
);
var indicatorCenter = const Offset(600, 30);
const includedIndicatorSize = Size(64, 32);
const excludedIndicatorSize = Size(74, 40);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
);
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
indicatorCenter = const Offset(600, 40);
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Make sure ripple is shifted when selectedIndex changes.
selectedIndex = 1;
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await tester.pumpAndSettle();
indicatorCenter = const Offset(600, 30);
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
});
testWidgets('Material3 - Navigation indicator ripple golden test', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/117420.
Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: Center(
child: NavigationBar(
labelBehavior: labelBehavior,
destinations: const <Widget>[
NavigationDestination(icon: SizedBox(), label: 'AC'),
NavigationDestination(icon: SizedBox(), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildWidget());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysShow_m3.png'));
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysHide_m3.png'));
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
await tester.pumpAndSettle();
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_onlyShowSelected_selected_m3.png'),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_onlyShowSelected_unselected_m3.png'),
);
});
// Regression test for https://github.com/flutter/flutter/issues/169249.
testWidgets('Material3 - Navigation indicator moves to selected item', (
WidgetTester tester,
) async {
final theme = ThemeData();
var index = 0;
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: RepaintBoundary(
child: NavigationBar(
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
selectedIndex: index,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
// Move the selection to the second destination.
index = 1;
await tester.pumpWidget(buildNavigationBar());
await tester.pumpAndSettle();
// The navigation indicator should be on the second item.
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('m3.navigation_bar.indicator.ink.position.png'),
);
});
testWidgets('Navigation indicator scale transform', (WidgetTester tester) async {
var selectedIndex = 0;
Widget buildNavigationBar() {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: Center(
child: NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
await tester.pumpAndSettle();
final Finder transformFinder = find
.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Transform))
.last;
Matrix4 transform = tester.widget<Transform>(transformFinder).transform;
expect(transform.getColumn(0)[0], 0.0);
selectedIndex = 1;
await tester.pumpWidget(buildNavigationBar());
await tester.pump(const Duration(milliseconds: 100));
transform = tester.widget<Transform>(transformFinder).transform;
expect(transform.getColumn(0)[0], closeTo(0.7805849514007568, precisionErrorTolerance));
await tester.pump(const Duration(milliseconds: 100));
transform = tester.widget<Transform>(transformFinder).transform;
expect(transform.getColumn(0)[0], closeTo(0.9473570239543915, precisionErrorTolerance));
await tester.pumpAndSettle();
transform = tester.widget<Transform>(transformFinder).transform;
expect(transform.getColumn(0)[0], 1.0);
});
testWidgets('Material3 - Navigation destination updates indicator color and shape', (
WidgetTester tester,
) async {
final theme = ThemeData();
const color = Color(0xff0000ff);
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: RepaintBoundary(
child: NavigationBar(
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
// Test default indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
await tester.pumpAndSettle();
// Test default indicator color and shape with ripple.
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('m3.navigation_bar.default.indicator.inkwell.shape.png'),
);
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
// Test custom indicator color and shape with ripple.
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('m3.navigation_bar.custom.indicator.inkwell.shape.png'),
);
});
testWidgets('Destinations respect their disabled state', (WidgetTester tester) async {
var selectedIndex = 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'),
NavigationDestination(icon: Icon(Icons.bookmark), label: 'Bookmark', enabled: false),
],
onDestinationSelected: (int i) => selectedIndex = i,
selectedIndex: selectedIndex,
),
),
);
await tester.tap(find.text('AC'));
expect(selectedIndex, 0);
await tester.tap(find.text('Alarm'));
expect(selectedIndex, 1);
await tester.tap(find.text('Bookmark'));
expect(selectedIndex, 1);
});
testWidgets('NavigationBar respects overlayColor in active/pressed/hovered states', (
WidgetTester tester,
) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const hoverColor = Color(0xff0000ff);
const focusColor = Color(0xff00ffff);
const pressedColor = Color(0xffff00ff);
final WidgetStateProperty<Color?> overlayColor = WidgetStateProperty.resolveWith<Color>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.hovered)) {
return hoverColor;
}
if (states.contains(WidgetState.focused)) {
return focusColor;
}
if (states.contains(WidgetState.pressed)) {
return pressedColor;
}
return Colors.transparent;
});
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: RepaintBoundary(
child: NavigationBar(
overlayColor: overlayColor,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
);
// Test hovered state.
expect(
inkFeatures,
kIsWeb
? (paints
..rrect()
..rrect()
..circle(color: hoverColor))
: (paints..circle(color: hoverColor)),
);
await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
await tester.pumpAndSettle();
// Test pressed state.
expect(
inkFeatures,
kIsWeb
? (paints
..circle()
..circle()
..circle(color: pressedColor))
: (paints
..circle()
..circle(color: pressedColor)),
);
await gesture.up();
await tester.pumpAndSettle();
// Press tab to focus the navigation bar.
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
// Test focused state.
expect(
inkFeatures,
kIsWeb
? (paints
..circle()
..circle(color: focusColor))
: (paints
..circle()
..circle(color: focusColor)),
);
});
testWidgets('NavigationBar.labelPadding overrides NavigationDestination.label padding', (
WidgetTester tester,
) async {
const EdgeInsetsGeometry labelPadding = EdgeInsets.all(8);
Widget buildNavigationBar({EdgeInsetsGeometry? labelPadding}) {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBar(
labelPadding: labelPadding,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
],
onDestinationSelected: (int i) {},
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
expect(_getLabelPadding(tester, 'Home'), const EdgeInsets.only(top: 4));
expect(_getLabelPadding(tester, 'Settings'), const EdgeInsets.only(top: 4));
await tester.pumpWidget(buildNavigationBar(labelPadding: labelPadding));
expect(_getLabelPadding(tester, 'Home'), labelPadding);
expect(_getLabelPadding(tester, 'Settings'), labelPadding);
});
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests
// can be deleted.
testWidgets('Material2 - Navigation destination updates indicator color and shape', (
WidgetTester tester,
) async {
final theme = ThemeData(useMaterial3: false);
const color = Color(0xff0000ff);
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: NavigationBar(
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
// Test default indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
expect(
_getIndicatorDecoration(tester)?.shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
);
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
});
testWidgets('Material2 - Navigation indicator renders ripple', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/116751.
var selectedIndex = 0;
Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
bottomNavigationBar: Center(
child: NavigationBar(
selectedIndex: selectedIndex,
labelBehavior: labelBehavior,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildWidget());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
);
var indicatorCenter = const Offset(600, 33);
const includedIndicatorSize = Size(64, 32);
const excludedIndicatorSize = Size(74, 40);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
);
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
indicatorCenter = const Offset(600, 40);
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
await tester.pumpAndSettle();
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
// Make sure ripple is shifted when selectedIndex changes.
selectedIndex = 1;
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await tester.pumpAndSettle();
indicatorCenter = const Offset(600, 33);
expect(
inkFeatures,
paints
..clipPath(
pathMatcher: isPathThat(
includes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
],
excludes: <Offset>[
// Left center.
Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Top center.
Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
// Right center.
Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
// Bottom center.
Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
],
),
)
..circle(
x: indicatorCenter.dx,
y: indicatorCenter.dy,
radius: 35.0,
color: const Color(0x0a000000),
),
);
});
testWidgets('Material2 - Navigation indicator ripple golden test', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/117420.
Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
bottomNavigationBar: Center(
child: NavigationBar(
labelBehavior: labelBehavior,
destinations: const <Widget>[
NavigationDestination(icon: SizedBox(), label: 'AC'),
NavigationDestination(icon: SizedBox(), label: 'Alarm'),
],
onDestinationSelected: (int i) {},
),
),
),
);
}
await tester.pumpWidget(buildWidget());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_alwaysShow_m2.png'),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_alwaysHide_m2.png'),
);
// Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
await tester.pumpWidget(
buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
await tester.pumpAndSettle();
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_onlyShowSelected_selected_m2.png'),
);
await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
await tester.pumpAndSettle();
await expectLater(
find.byType(NavigationBar),
matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png'),
);
});
testWidgets('Destination icon does not rebuild when tapped', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/122811.
Widget buildNavigationBar() {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
var selectedIndex = 0;
return NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: IconWithRandomColor(icon: Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: IconWithRandomColor(icon: Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {
setState(() {
selectedIndex = i;
});
},
);
},
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
Icon icon = tester.widget<Icon>(find.byType(Icon).last);
final Color initialColor = icon.color!;
// Trigger a rebuild.
await tester.tap(find.text('Alarm'));
await tester.pumpAndSettle();
// Icon color should be the same as before the rebuild.
icon = tester.widget<Icon>(find.byType(Icon).last);
expect(icon.color, initialColor);
});
});
testWidgets('NavigationBar.labelPadding overrides NavigationDestination.label padding', (
WidgetTester tester,
) async {
const selectedText = 'Home';
const unselectedText = 'Settings';
const EdgeInsetsGeometry labelPadding = EdgeInsets.all(8);
Widget buildNavigationBar({EdgeInsetsGeometry? labelPadding}) {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBar(
labelPadding: labelPadding,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.home), label: selectedText),
NavigationDestination(icon: Icon(Icons.settings), label: unselectedText),
],
onDestinationSelected: (int i) {},
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
expect(_getLabelPadding(tester, selectedText), const EdgeInsets.only(top: 4));
expect(_getLabelPadding(tester, unselectedText), const EdgeInsets.only(top: 4));
await tester.pumpWidget(buildNavigationBar(labelPadding: labelPadding));
expect(_getLabelPadding(tester, selectedText), labelPadding);
expect(_getLabelPadding(tester, unselectedText), labelPadding);
});
testWidgets('NavigationBar.labelTextStyle overrides NavigationDestination.label text style', (
WidgetTester tester,
) async {
const selectedText = 'Home';
const unselectedText = 'Settings';
const disabledText = 'Bookmark';
final theme = ThemeData();
Widget buildNavigationBar({WidgetStateProperty<TextStyle?>? labelTextStyle}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: NavigationBar(
labelTextStyle: labelTextStyle,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.home), label: selectedText),
NavigationDestination(icon: Icon(Icons.settings), label: unselectedText),
NavigationDestination(
enabled: false,
icon: Icon(Icons.bookmark),
label: disabledText,
),
],
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
// Test selected label text style.
expect(_getLabelStyle(tester, selectedText).fontSize, equals(12.0));
expect(_getLabelStyle(tester, selectedText).color, equals(theme.colorScheme.onSurface));
// Test unselected label text style.
expect(_getLabelStyle(tester, unselectedText).fontSize, equals(12.0));
expect(
_getLabelStyle(tester, unselectedText).color,
equals(theme.colorScheme.onSurfaceVariant),
);
// Test disabled label text style.
expect(_getLabelStyle(tester, disabledText).fontSize, equals(12.0));
expect(
_getLabelStyle(tester, disabledText).color,
equals(theme.colorScheme.onSurfaceVariant.withOpacity(0.38)),
);
const selectedTextStyle = TextStyle(fontSize: 15, color: Color(0xFF00FF00));
const unselectedTextStyle = TextStyle(fontSize: 15, color: Color(0xFF0000FF));
const disabledTextStyle = TextStyle(fontSize: 16, color: Color(0xFFFF0000));
await tester.pumpWidget(
buildNavigationBar(
labelTextStyle:
const WidgetStateProperty<TextStyle?>.fromMap(<WidgetStatesConstraint, TextStyle?>{
WidgetState.disabled: disabledTextStyle,
WidgetState.selected: selectedTextStyle,
WidgetState.any: unselectedTextStyle,
}),
),
);
// Test selected label text style.
expect(_getLabelStyle(tester, selectedText).fontSize, equals(selectedTextStyle.fontSize));
expect(_getLabelStyle(tester, selectedText).color, equals(selectedTextStyle.color));
// Test unselected label text style.
expect(_getLabelStyle(tester, unselectedText).fontSize, equals(unselectedTextStyle.fontSize));
expect(_getLabelStyle(tester, unselectedText).color, equals(unselectedTextStyle.color));
// Test disabled label text style.
expect(_getLabelStyle(tester, disabledText).fontSize, equals(disabledTextStyle.fontSize));
expect(_getLabelStyle(tester, disabledText).color, equals(disabledTextStyle.color));
});
testWidgets('NavigationBar.maintainBottomViewPadding can consume bottom MediaQuery.padding', (
WidgetTester tester,
) async {
const double bottomPadding = 40;
const TextDirection textDirection = TextDirection.ltr;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: textDirection,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
child: Scaffold(
bottomNavigationBar: NavigationBar(
maintainBottomViewPadding: true,
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
],
),
),
),
),
),
);
final double safeAreaBottomPadding = tester
.widget<Padding>(find.byType(Padding).first)
.padding
.resolve(textDirection)
.bottom;
expect(safeAreaBottomPadding, equals(0));
});
testWidgets('NavigationBar does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox.shrink(
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.add), label: 'X'),
NavigationDestination(icon: Icon(Icons.abc), label: 'Y'),
],
),
),
),
),
);
expect(tester.getSize(find.byType(NavigationBar)), Size.zero);
});
}
Widget _buildWidget(Widget child, {bool? useMaterial3}) {
return MaterialApp(
theme: ThemeData(useMaterial3: useMaterial3),
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? _getIndicatorDecoration(WidgetTester tester) {
return tester
.firstWidget<Ink>(
find.descendant(of: find.byType(FadeTransition), matching: find.byType(Ink)),
)
.decoration
as ShapeDecoration?;
}
class IconWithRandomColor extends StatelessWidget {
const IconWithRandomColor({super.key, required this.icon});
final IconData icon;
@override
Widget build(BuildContext context) {
final Color randomColor = Color(
(Random().nextDouble() * 0xFFFFFF).toInt(),
).withValues(alpha: 1.0);
return Icon(icon, color: randomColor);
}
}
bool _sizeAlmostEqual(Size a, Size b, {double maxDiff = 0.05}) {
return (a.width - b.width).abs() <= maxDiff && (a.height - b.height).abs() <= maxDiff;
}
EdgeInsetsGeometry _getLabelPadding(WidgetTester tester, String text) {
return tester
.widget<Padding>(find.ancestor(of: find.text(text), matching: find.byType(Padding)).first)
.padding;
}
TextStyle _getLabelStyle(WidgetTester tester, String text) {
return tester
.widget<RichText>(find.descendant(of: find.text(text), matching: find.byType(RichText)))
.text
.style!;
}