From 59d4d76705691930d0751016a25c6a4bca865e50 Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Mon, 6 Nov 2023 16:15:07 +0100 Subject: [PATCH] Update BottomNavigationBar tests for M3 (#136624) This PR updates `BottomNavigationBar` unit tests for M3 migration. More info in https://github.com/flutter/flutter/issues/127064 It was somewhat complex because existing tests relied on a lot of magic numbers. --- .../src/material/bottom_navigation_bar.dart | 2 +- .../material/bottom_navigation_bar_test.dart | 663 +++++++++++++++++- 2 files changed, 642 insertions(+), 23 deletions(-) diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index f642aaa5b629..dd4f4f04070b 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -1159,7 +1159,7 @@ class _BottomNavigationBarState extends State with TickerPr removeBottom: true, child: DefaultTextStyle.merge( overflow: TextOverflow.ellipsis, - child: Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: _createTiles(layout), ), diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart index 88f772474a7e..8e434ac5c4db 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart @@ -7,8 +7,10 @@ @Tags(['reduced-test-set']) library; +import 'dart:math'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -49,7 +51,7 @@ void main() { expect(mutatedIndex, 1); }); - testWidgetsWithLeakTracking('BottomNavigationBar content test', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar content test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -76,7 +78,34 @@ void main() { expect(find.text('Alarm'), findsOneWidget); }); - testWidgetsWithLeakTracking('Fixed BottomNavigationBar defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar content test', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + BottomNavigationBarItem( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + ), + ), + ), + ); + + final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(box.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + expect(find.text('AC'), findsOneWidget); + expect(find.text('Alarm'), findsOneWidget); + }); + + testWidgetsWithLeakTracking('Material2 - Fixed BottomNavigationBar defaults', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000001); const Color unselectedWidgetColor = Color(0xFF000002); @@ -141,6 +170,71 @@ void main() { expect(_getMaterial(tester).elevation, equals(8.0)); }); + testWidgetsWithLeakTracking('Material3 - Fixed BottomNavigationBar defaults', (WidgetTester tester) async { + const Color primaryColor = Color(0xFF000001); + const Color unselectedWidgetColor = Color(0xFF000002); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: const ColorScheme.light().copyWith(primary: primaryColor), + unselectedWidgetColor: unselectedWidgetColor, + ), + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + BottomNavigationBarItem( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + ), + ), + ), + ); + + const double selectedFontSize = 14.0; + const double unselectedFontSize = 12.0; + final TextStyle selectedFontStyle = tester.renderObject(find.text('AC')).text.style!; + final TextStyle unselectedFontStyle = tester.renderObject(find.text('Alarm')).text.style!; + final TextStyle selectedIcon = _iconStyle(tester, Icons.ac_unit); + final TextStyle unselectedIcon = _iconStyle(tester, Icons.access_alarm); + expect(selectedFontStyle.color, equals(primaryColor)); + expect(selectedFontStyle.fontSize, selectedFontSize); + expect(selectedFontStyle.fontWeight, equals(FontWeight.w400)); + expect(selectedFontStyle.height, 1.43); + expect(unselectedFontStyle.color, equals(unselectedWidgetColor)); + expect(unselectedFontStyle.fontWeight, equals(FontWeight.w400)); + expect(unselectedFontStyle.height, 1.43); + // Unselected label has a font size of 14 but is scaled down to be font size 12. + expect( + tester.firstWidget(find.ancestor(of: find.text('Alarm'), matching: find.byType(Transform))).transform, + equals(Matrix4.diagonal3(Vector3.all(unselectedFontSize / selectedFontSize))), + ); + expect(selectedIcon.color, equals(primaryColor)); + expect(selectedIcon.fontSize, equals(24.0)); + expect(unselectedIcon.color, equals(unselectedWidgetColor)); + expect(unselectedIcon.fontSize, equals(24.0)); + // There should not be any [Opacity] or [FadeTransition] widgets + // since showUnselectedLabels and showSelectedLabels are true. + final Finder findOpacity = find.descendant( + of: find.byType(BottomNavigationBar), + matching: find.byType(Opacity), + ); + final Finder findFadeTransition = find.descendant( + of: find.byType(BottomNavigationBar), + matching: find.byType(FadeTransition), + ); + expect(findOpacity, findsNothing); + expect(findFadeTransition, findsNothing); + expect(_getMaterial(tester).elevation, equals(8.0)); + }); + testWidgetsWithLeakTracking('Custom selected and unselected font styles', (WidgetTester tester) async { const TextStyle selectedTextStyle = TextStyle(fontWeight: FontWeight.w200, fontSize: 18.0); const TextStyle unselectedTextStyle = TextStyle(fontWeight: FontWeight.w600, fontSize: 12.0); @@ -411,7 +505,7 @@ void main() { expect(unselectedItemPadding.bottom, equals((selectedIconSize - unselectedIconSize) / 2.0)); }); - testWidgetsWithLeakTracking('Shifting BottomNavigationBar defaults', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - Shifting BottomNavigationBar defaults', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -440,6 +534,35 @@ void main() { expect(_getMaterial(tester).elevation, equals(8.0)); }); + testWidgetsWithLeakTracking('Material3 - Shifting BottomNavigationBar defaults', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.shifting, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + BottomNavigationBarItem( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + ), + ), + ), + ); + + const double selectedFontSize = 14.0; + expect(tester.renderObject(find.text('AC')).text.style!.fontSize, selectedFontSize); + final ThemeData theme = Theme.of(tester.element(find.text('AC'))); + expect(tester.renderObject(find.text('AC')).text.style!.color, equals(theme.colorScheme.surface)); + expect(_getOpacity(tester, 'Alarm'), equals(0.0)); + expect(_getMaterial(tester).elevation, equals(8.0)); + }); + testWidgetsWithLeakTracking('Fixed BottomNavigationBar custom font size, color', (WidgetTester tester) async { const Color primaryColor = Color(0xFF000000); const Color unselectedWidgetColor = Color(0xFFD501FF); @@ -990,7 +1113,7 @@ void main() { expect(_getMaterial(tester).elevation, equals(customElevation)); }); - testWidgetsWithLeakTracking('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1018,6 +1141,33 @@ void main() { expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); }); + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(viewPadding: EdgeInsets.only(bottom: 40.0)), + child: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + BottomNavigationBarItem( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + ), + ), + ), + ), + ); + + const double expectedMinHeight = kBottomNavigationBarHeight + 40.0; + expect(tester.getSize(find.byType(BottomNavigationBar)).height >= expectedMinHeight, isTrue); + }); + testWidgetsWithLeakTracking('BottomNavigationBar adds bottom padding to height with a custom font size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1323,7 +1473,7 @@ void main() { expect(builderIconSize, 12.0); }); - testWidgetsWithLeakTracking('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1344,10 +1494,8 @@ void main() { ), ), ); - final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar)); expect(defaultBox.size.height, equals(kBottomNavigationBarHeight)); - await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -1367,10 +1515,8 @@ void main() { ), ), ); - final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar)); expect(shiftingBox.size.height, equals(kBottomNavigationBarHeight)); - await tester.pumpWidget( MaterialApp( home: MediaQuery( @@ -1397,7 +1543,81 @@ void main() { expect(box.size.height, equals(56.0)); }); - testWidgetsWithLeakTracking('BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ); + final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(defaultBox.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.shifting, + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ); + final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(shiftingBox.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(textScaleFactor: 2.0), + child: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ), + ); + + final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(box.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + }); + + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), @@ -1448,7 +1668,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: MediaQuery( - data: const MediaQueryData(textScaleFactor: 2.0), + data: const MediaQueryData(textScaler: TextScaler.linear(2.0)), child: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ @@ -1471,7 +1691,83 @@ void main() { expect(box.size.height, equals(kBottomNavigationBarHeight)); }); - testWidgetsWithLeakTracking('BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ); + + final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(defaultBox.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.shifting, + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ); + + final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar)); + // kBottomNavigationBarHeight is a minimum dimension. + expect(shiftingBox.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); + + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(textScaler: TextScaler.linear(2.0)), + child: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + label: 'A', + icon: Icon(Icons.ac_unit), + ), + BottomNavigationBarItem( + label: 'B', + icon: Icon(Icons.battery_alert), + ), + ], + ), + ), + ), + ), + ); + + final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); + expect(box.size.height, equals(defaultBox.size.height)); + expect(box.size.height, equals(shiftingBox.size.height)); + }); + + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async { const String label = 'Foo'; Widget buildApp({ required double textScaleFactor }) { @@ -1529,6 +1825,63 @@ void main() { expect(tester.getSize(find.text(label).last), equals(const Size(168.0, 56.0))); }); + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async { + const String label = 'Foo'; + + Widget buildApp({ required double textScaleFactor }) { + return MediaQuery( + data: MediaQueryData(textScaleFactor: textScaleFactor), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + label: label, + icon: Icon(Icons.ac_unit), + tooltip: label, + ), + BottomNavigationBarItem( + 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)); + expect(tester.getSize(find.text(label).last).height, equals(20.0)); + 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).height, equals(80.0)); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + testWidgetsWithLeakTracking('Different behaviour of tool tip in BottomNavigationBarItem', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1574,7 +1927,6 @@ void main() { await tester.pumpWidget( MaterialApp( - theme: ThemeData(useMaterial3: false), home: Scaffold( bottomNavigationBar: BottomNavigationBar( items: [ @@ -1593,15 +1945,15 @@ void main() { ); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); - expect(box.size.height, equals(kBottomNavigationBarHeight)); + expect(box.size.height, greaterThanOrEqualTo(kBottomNavigationBarHeight)); final RenderBox itemBoxA = tester.renderObject(find.text(longTextA)); - expect(itemBoxA.size, equals(const Size(400.0, 14.0))); + expect(itemBoxA.size.width, equals(400.0)); final RenderBox itemBoxB = tester.renderObject(find.text(longTextB)); - expect(itemBoxB.size, equals(const Size(400.0, 14.0))); + expect(itemBoxB.size.width, equals(400.0)); }); - testWidgetsWithLeakTracking('BottomNavigationBar paints circles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar paints circles', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( useMaterial3: false, @@ -1637,6 +1989,7 @@ void main() { // Now we flip the directionality and verify that the circles switch positions. await tester.pumpWidget( boilerplate( + useMaterial3: false, textDirection: TextDirection.rtl, bottomNavigationBar: BottomNavigationBar( items: const [ @@ -1929,7 +2282,7 @@ void main() { expect(tester.widget(backgroundMaterial).color, Colors.green); }); - group('BottomNavigationBar shifting backgroundColor with transition', () { + group('Material2 - BottomNavigationBar shifting backgroundColor with transition', () { // Regression test for: https://github.com/flutter/flutter/issues/22226 Widget runTest() { int currentIndex = 0; @@ -1976,7 +2329,59 @@ void main() { } await expectLater( find.byType(BottomNavigationBar), - matchesGoldenFile('bottom_navigation_bar.shifting_transition.${pump - 1}.png'), + matchesGoldenFile('m2_bottom_navigation_bar.shifting_transition.${pump - 1}.png'), + ); + }); + } + }); + + group('Material3 - BottomNavigationBar shifting backgroundColor with transition', () { + // Regression test for: https://github.com/flutter/flutter/issues/22226 + Widget runTest() { + int currentIndex = 0; + return MaterialApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Scaffold( + bottomNavigationBar: RepaintBoundary( + child: BottomNavigationBar( + type: BottomNavigationBarType.shifting, + currentIndex: currentIndex, + onTap: (int index) { + setState(() { + currentIndex = index; + }); + }, + items: const [ + BottomNavigationBarItem( + label: 'Red', + backgroundColor: Colors.red, + icon: Icon(Icons.dashboard), + ), + BottomNavigationBarItem( + label: 'Green', + backgroundColor: Colors.green, + icon: Icon(Icons.menu), + ), + ], + ), + ), + ); + }, + ), + ); + } + for (int pump = 1; pump < 9; pump++) { + testWidgetsWithLeakTracking('pump $pump', (WidgetTester tester) async { + await tester.pumpWidget(runTest()); + await tester.tap(find.text('Green')); + + for (int i = 0; i < pump; i++) { + await tester.pump(const Duration(milliseconds: 30)); + } + await expectLater( + find.byType(BottomNavigationBar), + matchesGoldenFile('m3_bottom_navigation_bar.shifting_transition.${pump - 1}.png'), ); }); } @@ -2368,7 +2773,7 @@ void main() { semantics.dispose(); }); - testWidgetsWithLeakTracking('BottomNavigationBar default layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar default layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); @@ -2416,7 +2821,74 @@ void main() { expect(tester.getRect(find.byKey(icon1)), const Rect.fromLTRB(500.0, 560.0, 700.0, 570.0)); }); - testWidgetsWithLeakTracking('BottomNavigationBar centered landscape layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar default layout', (WidgetTester tester) async { + final Key icon0 = UniqueKey(); + final Key icon1 = UniqueKey(); + const double iconHeight = 10; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold( + bottomNavigationBar: BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: SizedBox(key: icon0, width: 200, height: iconHeight), + label: 'Title0', + ), + BottomNavigationBarItem( + icon: SizedBox(key: icon1, width: 200, height: iconHeight), + label: 'Title1', + ), + ], + ), + ); + }, + ), + ), + ); + expect(tester.getSize(find.byType(BottomNavigationBar)), const Size(800, kBottomNavigationBarHeight)); + expect(tester.getRect(find.byType(BottomNavigationBar)), const Rect.fromLTRB(0, 600 - kBottomNavigationBarHeight, 800, 600)); + + const double navigationBarTop = 600 - kBottomNavigationBarHeight; // 544 + const double selectedFontSize = 14.0; + const double m3LineHeight = 1.43; + final double labelHeight = (selectedFontSize * m3LineHeight).floorToDouble(); // 20 + const double navigationTileVerticalPadding = selectedFontSize / 2; // 7.0 + final double navigationTileHeight = iconHeight + labelHeight + 2 * navigationTileVerticalPadding; + + // Navigation tiles parent is a Row with crossAxisAlignment set to center. + final double navigationTileVerticalOffset = (kBottomNavigationBarHeight - navigationTileHeight) / 2; + + final double iconTop = navigationBarTop + navigationTileVerticalOffset + navigationTileVerticalPadding; + final double labelBottom = 600 - (navigationTileVerticalOffset + navigationTileVerticalPadding); + + expect(tester.getRect(find.byKey(icon0)).top, iconTop); + expect(tester.getRect(find.text('Title0')).bottom, labelBottom); + + // The items are padded horizontally according to + // MainAxisAlignment.spaceAround. Left/right padding is: + // 800 - (200 * 2) / 4 = 100 + // The layout of the unselected item's label is slightly different; not + // checking that here. + final double firstLabelWidth = tester.getSize(find.text('Title0')).width; + const double itemsWidth = 800 / 2; // 2 items. + const double firstLabelCenter = itemsWidth / 2; + expect( + tester.getRect(find.text('Title0')), + Rect.fromLTRB( + firstLabelCenter - firstLabelWidth / 2, + labelBottom - labelHeight, + firstLabelCenter + firstLabelWidth / 2, + labelBottom, + ), + ); + expect(tester.getRect(find.byKey(icon0)), Rect.fromLTRB(100.0, iconTop, 300.0, iconTop + iconHeight)); + expect(tester.getRect(find.byKey(icon1)), Rect.fromLTRB(500.0, iconTop, 700.0, iconTop + iconHeight)); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar centered landscape layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); @@ -2462,7 +2934,80 @@ void main() { expect(tester.getRect(find.byKey(icon1)), const Rect.fromLTRB(450.0, 560.0, 650.0, 570.0)); }); - testWidgetsWithLeakTracking('BottomNavigationBar linear landscape layout', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar centered landscape layout', (WidgetTester tester) async { + final Key icon0 = UniqueKey(); + final Key icon1 = UniqueKey(); + const double iconWidth = 200; + const double iconHeight = 10; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold( + bottomNavigationBar: BottomNavigationBar( + landscapeLayout: BottomNavigationBarLandscapeLayout.centered, + items: [ + BottomNavigationBarItem( + icon: SizedBox(key: icon0, width: iconWidth, height: iconHeight), + label: 'Title0', + ), + BottomNavigationBarItem( + icon: SizedBox(key: icon1, width: iconWidth, height: iconHeight), + label: 'Title1', + ), + ], + ), + ); + }, + ), + ), + ); + + expect(tester.getSize(find.byType(BottomNavigationBar)), const Size(800, kBottomNavigationBarHeight)); + expect(tester.getRect(find.byType(BottomNavigationBar)), const Rect.fromLTRB(0, 600 - kBottomNavigationBarHeight, 800, 600)); + + const double navigationBarTop = 600 - kBottomNavigationBarHeight; // 544 + const double selectedFontSize = 14.0; + const double m3LineHeight = 1.43; + final double labelHeight = (selectedFontSize * m3LineHeight).floorToDouble(); // 20 + const double navigationTileVerticalPadding = selectedFontSize / 2; // 7.0 + final double navigationTileHeight = iconHeight + labelHeight + 2 * navigationTileVerticalPadding; + + // Navigation tiles parent is a Row with crossAxisAlignment sets to center. + final double navigationTileVerticalOffset = (kBottomNavigationBarHeight - navigationTileHeight) / 2; + + final double iconTop = navigationBarTop + navigationTileVerticalOffset + navigationTileVerticalPadding; + final double labelBottom = 600 - (navigationTileVerticalOffset + navigationTileVerticalPadding); + + // The items are laid out as in the default case, within width = 600 + // (the "portrait" width) and the result is centered with the + // landscape width = 800. + // So item 0's left edges are: + // ((800 - 600) / 2) + ((600 - 400) / 4) = 150. + // Item 1's right edge is: + // 800 - 150 = 650 + // The layout of the unselected item's label is slightly different; not + // checking that here. + final double firstLabelWidth = tester.getSize(find.text('Title0')).width; + const double itemWidth = iconWidth; // 200 + const double firstItemLeft = 150; + const double firstLabelCenter = firstItemLeft + itemWidth / 2; // 250 + + expect(tester.getRect( + find.text('Title0')), + Rect.fromLTRB( + firstLabelCenter - firstLabelWidth / 2, + labelBottom - labelHeight, + firstLabelCenter + firstLabelWidth / 2, + labelBottom, + ), + ); + expect(tester.getRect(find.byKey(icon0)), Rect.fromLTRB(150.0, iconTop, 350.0, iconTop + iconHeight)); + expect(tester.getRect(find.byKey(icon1)), Rect.fromLTRB(450.0, iconTop, 650.0, iconTop + iconHeight)); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + + testWidgetsWithLeakTracking('Material2 - BottomNavigationBar linear landscape layout', (WidgetTester tester) async { final Key icon0 = UniqueKey(); final Key icon1 = UniqueKey(); @@ -2502,6 +3047,80 @@ void main() { expect(tester.getRect(find.byKey(icon0)), const Rect.fromLTRB(104.0, 562.0, 204.0, 582.0)); expect(tester.getRect(find.byKey(icon1)), const Rect.fromLTRB(504.0, 562.0, 604.0, 582.0)); }); + + testWidgetsWithLeakTracking('Material3 - BottomNavigationBar linear landscape layout', (WidgetTester tester) async { + final Key icon0 = UniqueKey(); + final Key icon1 = UniqueKey(); + const double iconWidth = 100; + const double iconHeight = 20; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(), + home: Builder( + builder: (BuildContext context) { + return Scaffold( + bottomNavigationBar: BottomNavigationBar( + landscapeLayout: BottomNavigationBarLandscapeLayout.linear, + items: [ + BottomNavigationBarItem( + icon: SizedBox(key: icon0, width: iconWidth, height: iconHeight), + label: 'Title0', + ), + BottomNavigationBarItem( + icon: SizedBox(key: icon1, width: iconWidth, height: iconHeight), + label: 'Title1', + ), + ], + ), + ); + }, + ), + ), + ); + + expect(tester.getSize(find.byType(BottomNavigationBar)), const Size(800, kBottomNavigationBarHeight)); + expect(tester.getRect(find.byType(BottomNavigationBar)), const Rect.fromLTRB(0, 600 - kBottomNavigationBarHeight, 800, 600)); + + const double navigationBarTop = 600 - kBottomNavigationBarHeight; // 544 + const double selectedFontSize = 14.0; + const double m3LineHeight = 1.43; + final double labelHeight = (selectedFontSize * m3LineHeight).floorToDouble(); // 20 + const double navigationTileVerticalPadding = selectedFontSize / 2; // 7.0 + // Icon and label are in the same row. + final double navigationTileHeight = max(iconHeight, labelHeight) + 2 * navigationTileVerticalPadding; + + // Navigation tiles parent is a Row with crossAxisAlignment sets to center. + final double navigationTileVerticalOffset = (kBottomNavigationBarHeight - navigationTileHeight) / 2; + + final double iconTop = navigationBarTop + navigationTileVerticalOffset + navigationTileVerticalPadding; + final double labelBottom = 600 - (navigationTileVerticalOffset + navigationTileVerticalPadding); + + // The items are laid out as in the default case except each + // item's icon/label is arranged in a row, with 8 pixels in + // between the icon and label. The layout of the unselected + // item's label is slightly different; not checking that here. + const double itemFullWith = 800 / 2; // Two items in the navigation bar. + const double separatorWidth = 8; + final double firstLabelWidth = tester.getSize(find.text('Title0')).width; + final double firstItemContentWidth = iconWidth + separatorWidth + firstLabelWidth; + final double firstItemLeft = itemFullWith / 2 - firstItemContentWidth / 2; + final double secondLabelWidth = tester.getSize(find.text('Title1')).width; + final double secondItemContentWidth = iconWidth + separatorWidth + secondLabelWidth; + final double secondItemLeft = itemFullWith + itemFullWith / 2 - secondItemContentWidth / 2; + + expect(tester.getRect( + find.text('Title0')), + Rect.fromLTRB( + firstItemLeft + iconWidth + separatorWidth, + labelBottom - labelHeight, + firstItemLeft + iconWidth + separatorWidth + firstLabelWidth, + labelBottom, + ), + ); + expect(tester.getRect(find.byKey(icon0)), Rect.fromLTRB(firstItemLeft, iconTop, firstItemLeft + iconWidth, iconTop + iconHeight)); + expect(tester.getRect(find.byKey(icon1)), Rect.fromLTRB(secondItemLeft, iconTop, secondItemLeft + iconWidth, iconTop + iconHeight)); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 } Widget boilerplate({ Widget? bottomNavigationBar, required TextDirection textDirection, bool? useMaterial3 }) {