// 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(['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 [ 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 [ 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 [ 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 [ 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 [ 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 [ 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 [ 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 [ 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 >[ DefaultMaterialLocalizations.delegate, DefaultWidgetsLocalizations.delegate, ], child: MaterialApp( theme: ThemeData(useMaterial3: false), home: Navigator( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { return Scaffold( bottomNavigationBar: NavigationBar( destinations: const [ 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 >[ DefaultMaterialLocalizations.delegate, DefaultWidgetsLocalizations.delegate, ], child: MaterialApp( home: Navigator( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { return Scaffold( bottomNavigationBar: NavigationBar( destinations: const [ 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 >[ DefaultMaterialLocalizations.delegate, DefaultWidgetsLocalizations.delegate, ], child: MaterialApp( home: Navigator( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { return Scaffold( bottomNavigationBar: NavigationBar( destinations: const [ 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(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 [ 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 [ 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 [ 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(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 [ 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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 [ 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 [ 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 [ 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(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(transformFinder).transform; expect(transform.getColumn(0)[0], closeTo(0.7805849514007568, precisionErrorTolerance)); await tester.pump(const Duration(milliseconds: 100)); transform = tester.widget(transformFinder).transform; expect(transform.getColumn(0)[0], closeTo(0.9473570239543915, precisionErrorTolerance)); await tester.pumpAndSettle(); transform = tester.widget(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 [ 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 [ 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 overlayColor = WidgetStateProperty.resolveWith(( Set 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 [ 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 [ 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 [ 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 [ 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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: [ // 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 [ 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 [ 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(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(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 [ 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? labelTextStyle}) { return MaterialApp( theme: theme, home: Scaffold( bottomNavigationBar: NavigationBar( labelTextStyle: labelTextStyle, destinations: const [ 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.fromMap({ 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 [ NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'), NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'), ], ), ), ), ), ), ); final double safeAreaBottomPadding = tester .widget(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 [ 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( find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)), ); } ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( 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(find.ancestor(of: find.text(text), matching: find.byType(Padding)).first) .padding; } TextStyle _getLabelStyle(WidgetTester tester, String text) { return tester .widget(find.descendant(of: find.text(text), matching: find.byType(RichText))) .text .style!; }