| // 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!; |
| } |