// Copyright 2016 The Chromium 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 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { testWidgets('BottomNavigationBar callback test', (WidgetTester tester) async { int mutatedIndex; await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ) ], onTap: (int index) { mutatedIndex = index; } ) ) ) ); await tester.tap(find.text('Alarm')); expect(mutatedIndex, 1); }); testWidgets('BottomNavigationBar content test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ) ] ) ) ) ); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); expect(box.size.height, kBottomNavigationBarHeight); expect(find.text('AC'), findsOneWidget); expect(find.text('Alarm'), findsOneWidget); }); testWidgets('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: MediaQuery( data: const MediaQueryData(padding: EdgeInsets.only(bottom: 40.0)), child: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ) ] ) ) ) ) ); const double labelBottomMargin = 8.0; // _kBottomMargin in implementation. const double additionalPadding = 40.0 - labelBottomMargin; const double expectedHeight = kBottomNavigationBarHeight + additionalPadding; expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); }); testWidgets('BottomNavigationBar action size test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ) ] ) ) ) ); Iterable actions = tester.renderObjectList(find.byType(InkResponse)); expect(actions.length, 2); expect(actions.elementAt(0).size.width, 480.0); expect(actions.elementAt(1).size.width, 320.0); await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( currentIndex: 1, type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ) ] ) ) ) ); await tester.pump(const Duration(milliseconds: 200)); actions = tester.renderObjectList(find.byType(InkResponse)); expect(actions.length, 2); expect(actions.elementAt(0).size.width, 320.0); expect(actions.elementAt(1).size.width, 480.0); }); testWidgets('BottomNavigationBar multiple taps test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ), BottomNavigationBarItem( icon: Icon(Icons.access_time), title: Text('Time') ), BottomNavigationBarItem( icon: Icon(Icons.add), title: Text('Add') ) ] ) ) ) ); // We want to make sure that the last label does not get displaced, // irrespective of how many taps happen on the first N - 1 labels and how // they grow. Iterable actions = tester.renderObjectList(find.byType(InkResponse)); final Offset originalOrigin = actions.elementAt(3).localToGlobal(Offset.zero); await tester.tap(find.text('AC')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); actions = tester.renderObjectList(find.byType(InkResponse)); expect(actions.elementAt(3).localToGlobal(Offset.zero), equals(originalOrigin)); await tester.tap(find.text('Alarm')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); actions = tester.renderObjectList(find.byType(InkResponse)); expect(actions.elementAt(3).localToGlobal(Offset.zero), equals(originalOrigin)); await tester.tap(find.text('Time')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); actions = tester.renderObjectList(find.byType(InkResponse)); expect(actions.elementAt(3).localToGlobal(Offset.zero), equals(originalOrigin)); }); testWidgets('BottomNavigationBar inherits shadowed app theme for shifting navbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), home: Theme( data: ThemeData(brightness: Brightness.dark), child: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ), BottomNavigationBarItem( icon: Icon(Icons.access_time), title: Text('Time') ), BottomNavigationBarItem( icon: Icon(Icons.add), title: Text('Add') ) ] ) ) ) ) ); await tester.tap(find.text('Alarm')); await tester.pump(const Duration(seconds: 1)); expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark)); }); testWidgets('BottomNavigationBar inherits shadowed app theme for fixed navbar', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), home: Theme( data: ThemeData(brightness: Brightness.dark), child: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC') ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm') ), BottomNavigationBarItem( icon: Icon(Icons.access_time), title: Text('Time') ), BottomNavigationBarItem( icon: Icon(Icons.add), title: Text('Add') ) ] ) ) ) ) ); await tester.tap(find.text('Alarm')); await tester.pump(const Duration(seconds: 1)); expect(Theme.of(tester.element(find.text('Alarm'))).brightness, equals(Brightness.dark)); }); testWidgets('BottomNavigationBar iconSize test', (WidgetTester tester) async { double builderIconSize; await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( iconSize: 12.0, items: [ const BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: const Text('B'), icon: Builder( builder: (BuildContext context) { builderIconSize = IconTheme.of(context).size; return SizedBox( width: builderIconSize, height: builderIconSize, ); }, ), ), ], ), ), ), ); final RenderBox box = tester.renderObject(find.byType(Icon)); expect(box.size.width, equals(12.0)); expect(box.size.height, equals(12.0)); expect(builderIconSize, 12.0); }); testWidgets('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: Text('B'), icon: Icon(Icons.battery_alert), ), ], ), ), ), ); final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar)); expect(defaultBox.size.height, equals(kBottomNavigationBarHeight)); await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: Text('B'), icon: Icon(Icons.battery_alert), ), ], ), ), ), ); final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar)); expect(shiftingBox.size.height, equals(kBottomNavigationBarHeight)); await tester.pumpWidget( MaterialApp( home: MediaQuery( data: const MediaQueryData(textScaleFactor: 2.0), child: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: Text('B'), icon: Icon(Icons.battery_alert), ), ], ), ), ), ), ); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); expect(box.size.height, equals(68.0)); }); testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async { final Text longTextA = Text(''.padLeft(100, 'A')); final Text longTextB = Text(''.padLeft(100, 'B')); await tester.pumpWidget( MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( items: [ BottomNavigationBarItem( title: longTextA, icon: const Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: longTextB, icon: const Icon(Icons.battery_alert), ), ], ), ), ), ); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); expect(box.size.height, equals(kBottomNavigationBarHeight)); final RenderBox itemBoxA = tester.renderObject(find.text(longTextA.data)); expect(itemBoxA.size, equals(const Size(400.0, 14.0))); final RenderBox itemBoxB = tester.renderObject(find.text(longTextB.data)); expect(itemBoxB.size, equals(const Size(400.0, 14.0))); }); testWidgets('BottomNavigationBar paints circles', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: Text('B'), icon: Icon(Icons.battery_alert), ), ], ), ), ); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); expect(box, isNot(paints..circle())); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(milliseconds: 20)); expect(box, paints..circle(x: 200.0)); await tester.tap(find.text('B')); await tester.pump(); await tester.pump(const Duration(milliseconds: 20)); expect(box, paints..circle(x: 200.0)..translate(x: 400.0)..circle(x: 200.0)); // Now we flip the directionality and verify that the circles switch positions. await tester.pumpWidget( boilerplate( textDirection: TextDirection.rtl, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( title: Text('A'), icon: Icon(Icons.ac_unit), ), BottomNavigationBarItem( title: Text('B'), icon: Icon(Icons.battery_alert), ), ], ), ), ); expect(box, paints..translate()..save()..translate(x: 400.0)..circle(x: 200.0)..restore()..circle(x: 200.0)); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(milliseconds: 20)); expect( box, paints ..translate(x: 0.0, y: 0.0) ..save() ..translate(x: 400.0) ..circle(x: 200.0) ..restore() ..circle(x: 200.0) ..translate(x: 400.0) ..circle(x: 200.0) ); }); testWidgets('BottomNavigationBar inactiveIcon shown', (WidgetTester tester) async { const Key filled = Key('filled'); const Key stroked = Key('stroked'); int selectedItem = 0; await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, bottomNavigationBar: BottomNavigationBar( currentIndex: selectedItem, items: const [ BottomNavigationBarItem( activeIcon: Icon(Icons.favorite, key: filled), icon: Icon(Icons.favorite_border, key: stroked), title: Text('Favorite'), ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm'), ), ], ), ), ); expect(find.byKey(filled), findsOneWidget); expect(find.byKey(stroked), findsNothing); selectedItem = 1; await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, bottomNavigationBar: BottomNavigationBar( currentIndex: selectedItem, items: const [ BottomNavigationBarItem( activeIcon: Icon(Icons.favorite, key: filled), icon: Icon(Icons.favorite_border, key: stroked), title: Text('Favorite'), ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm'), ), ], ), ), ); expect(find.byKey(filled), findsNothing); expect(find.byKey(stroked), findsOneWidget); }); testWidgets('BottomNavigationBar.fixed semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC'), ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm'), ), BottomNavigationBarItem( icon: Icon(Icons.hot_tub), title: Text('Hot Tub'), ), ], ), ), ); final TestSemantics expected = TestSemantics.root( children: [ TestSemantics( children: [ TestSemantics( children: [ TestSemantics( flags: [ SemanticsFlag.isSelected, SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'AC\nTab 1 of 3', textDirection: TextDirection.ltr, ), TestSemantics( flags: [ SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'Alarm\nTab 2 of 3', textDirection: TextDirection.ltr, ), TestSemantics( flags: [ SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'Hot Tub\nTab 3 of 3', textDirection: TextDirection.ltr, ), ], ), ], ), ], ); expect(semantics, hasSemantics(expected, ignoreId: true, ignoreTransform: true, ignoreRect: true)); semantics.dispose(); }); testWidgets('BottomNavigationBar.shifting semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( boilerplate( textDirection: TextDirection.ltr, bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: const [ BottomNavigationBarItem( icon: Icon(Icons.ac_unit), title: Text('AC'), ), BottomNavigationBarItem( icon: Icon(Icons.access_alarm), title: Text('Alarm'), ), BottomNavigationBarItem( icon: Icon(Icons.hot_tub), title: Text('Hot Tub'), ), ], ), ), ); final TestSemantics expected = TestSemantics.root( children: [ TestSemantics( children: [ TestSemantics( children: [ TestSemantics( flags: [ SemanticsFlag.isSelected, SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'AC\nTab 1 of 3', textDirection: TextDirection.ltr, ), TestSemantics( flags: [ SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'Alarm\nTab 2 of 3', textDirection: TextDirection.ltr, ), TestSemantics( flags: [ SemanticsFlag.isHeader, ], actions: [SemanticsAction.tap], label: 'Hot Tub\nTab 3 of 3', textDirection: TextDirection.ltr, ), ], ), ], ), ], ); expect(semantics, hasSemantics(expected, ignoreId: true, ignoreTransform: true, ignoreRect: true)); semantics.dispose(); }); testWidgets('BottomNavigationBar handles items.length changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/10322 Widget buildFrame(int itemCount) { return MaterialApp( home: Scaffold( bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: 0, items: List.generate(itemCount, (int itemIndex) { return BottomNavigationBarItem( icon: const Icon(Icons.android), title: Text('item $itemIndex'), ); }), ), ), ); } await tester.pumpWidget(buildFrame(3)); expect(find.text('item 0'), findsOneWidget); expect(find.text('item 1'), findsOneWidget); expect(find.text('item 2'), findsOneWidget); expect(find.text('item 3'), findsNothing); await tester.pumpWidget(buildFrame(4)); expect(find.text('item 0'), findsOneWidget); expect(find.text('item 1'), findsOneWidget); expect(find.text('item 2'), findsOneWidget); expect(find.text('item 3'), findsOneWidget); await tester.pumpWidget(buildFrame(2)); expect(find.text('item 0'), findsOneWidget); expect(find.text('item 1'), findsOneWidget); expect(find.text('item 2'), findsNothing); expect(find.text('item 3'), findsNothing); }); testWidgets('BottomNavigationBar change backgroundColor test', (WidgetTester tester) async { // Regression test for: https://github.com/flutter/flutter/issues/19653 Color _backgroundColor = Colors.red; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: Center( child: RaisedButton( child: const Text('green'), onPressed: () { setState(() { _backgroundColor = Colors.green; }); }, ), ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.shifting, items: [ BottomNavigationBarItem( title: const Text('Page 1'), backgroundColor: _backgroundColor, icon: const Icon(Icons.dashboard), ), BottomNavigationBarItem( title: const Text('Page 2'), backgroundColor: _backgroundColor, icon: const Icon(Icons.menu), ), ], ), ); }, ), ), ); final Finder backgroundMaterial = find.descendant( of: find.byType(BottomNavigationBar), matching: find.byWidgetPredicate((Widget w) { if (w is Material) return w.type == MaterialType.canvas; return false; }), ); expect(_backgroundColor, Colors.red); expect(tester.widget(backgroundMaterial).color, Colors.red); await tester.tap(find.text('green')); await tester.pumpAndSettle(); expect(_backgroundColor, Colors.green); expect(tester.widget(backgroundMaterial).color, Colors.green); }); } Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) { assert(textDirection != null); return Localizations( locale: const Locale('en', 'US'), delegates: const >[ DefaultMaterialLocalizations.delegate, DefaultWidgetsLocalizations.delegate, ], child: Directionality( textDirection: textDirection, child: MediaQuery( data: const MediaQueryData(), child: Material( child: Scaffold( bottomNavigationBar: bottomNavigationBar, ), ), ), ), ); }