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