1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This file is run as part of a reduced test set in CI on Mac and Windows
6// machines.
7@Tags(<String>['reduced-test-set'])
8library;
9
10import 'dart:math';
11
12import 'package:flutter/foundation.dart';
13import 'package:flutter/gestures.dart';
14import 'package:flutter/material.dart';
15import 'package:flutter/services.dart';
16import 'package:flutter_test/flutter_test.dart';
17
18void main() {
19 testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
20 int mutatedIndex = -1;
21 final Widget widget = _buildWidget(
22 NavigationBar(
23 destinations: const <Widget>[
24 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
25 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
26 ],
27 onDestinationSelected: (int i) {
28 mutatedIndex = i;
29 },
30 ),
31 );
32
33 await tester.pumpWidget(widget);
34
35 expect(find.text('AC'), findsOneWidget);
36 expect(find.text('Alarm'), findsOneWidget);
37
38 await tester.tap(find.text('Alarm'));
39 expect(mutatedIndex, 1);
40
41 await tester.tap(find.text('AC'));
42 expect(mutatedIndex, 0);
43 });
44
45 testWidgets('NavigationBar can update background color', (WidgetTester tester) async {
46 const Color color = Colors.yellow;
47
48 await tester.pumpWidget(
49 _buildWidget(
50 NavigationBar(
51 backgroundColor: color,
52 destinations: const <Widget>[
53 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
54 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
55 ],
56 onDestinationSelected: (int i) {},
57 ),
58 ),
59 );
60
61 expect(_getMaterial(tester).color, equals(color));
62 });
63
64 testWidgets('NavigationBar can update elevation', (WidgetTester tester) async {
65 const double elevation = 42.0;
66
67 await tester.pumpWidget(
68 _buildWidget(
69 NavigationBar(
70 elevation: elevation,
71 destinations: const <Widget>[
72 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
73 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
74 ],
75 onDestinationSelected: (int i) {},
76 ),
77 ),
78 );
79
80 expect(_getMaterial(tester).elevation, equals(elevation));
81 });
82
83 testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
84 const double bottomPadding = 40.0;
85
86 await tester.pumpWidget(
87 _buildWidget(
88 NavigationBar(
89 destinations: const <Widget>[
90 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
91 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
92 ],
93 onDestinationSelected: (int i) {},
94 ),
95 ),
96 );
97
98 final double defaultSize = tester.getSize(find.byType(NavigationBar)).height;
99 expect(defaultSize, 80);
100
101 await tester.pumpWidget(
102 _buildWidget(
103 MediaQuery(
104 data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
105 child: NavigationBar(
106 destinations: const <Widget>[
107 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
108 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
109 ],
110 onDestinationSelected: (int i) {},
111 ),
112 ),
113 ),
114 );
115
116 final double expectedHeight = defaultSize + bottomPadding;
117 expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
118 });
119
120 testWidgets('NavigationBar respects the notch/system navigation bar in landscape mode', (
121 WidgetTester tester,
122 ) async {
123 const double safeAreaPadding = 40.0;
124 Widget navigationBar() {
125 return NavigationBar(
126 destinations: const <Widget>[
127 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
128 NavigationDestination(
129 key: Key('Center'),
130 icon: Icon(Icons.center_focus_strong),
131 label: 'Center',
132 ),
133 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
134 ],
135 onDestinationSelected: (int i) {},
136 );
137 }
138
139 await tester.pumpWidget(_buildWidget(navigationBar()));
140 final double defaultWidth = tester.getSize(find.byType(NavigationBar)).width;
141 final Finder defaultCenterItem = find.byKey(const Key('Center'));
142 final Offset center = tester.getCenter(defaultCenterItem);
143 expect(center.dx, defaultWidth / 2);
144
145 await tester.pumpWidget(
146 _buildWidget(
147 MediaQuery(
148 data: const MediaQueryData(padding: EdgeInsets.only(left: safeAreaPadding)),
149 child: navigationBar(),
150 ),
151 ),
152 );
153
154 // The position of center item of navigation bar should indicate whether
155 // the safe area is sufficiently respected, when safe area is on the left side.
156 // e.g. Android device with system navigation bar in landscape mode.
157 final Finder leftPaddedCenterItem = find.byKey(const Key('Center'));
158 final Offset leftPaddedCenter = tester.getCenter(leftPaddedCenterItem);
159 expect(
160 leftPaddedCenter.dx,
161 closeTo((defaultWidth + safeAreaPadding) / 2.0, precisionErrorTolerance),
162 );
163
164 await tester.pumpWidget(
165 _buildWidget(
166 MediaQuery(
167 data: const MediaQueryData(padding: EdgeInsets.only(right: safeAreaPadding)),
168 child: navigationBar(),
169 ),
170 ),
171 );
172
173 // The position of center item of navigation bar should indicate whether
174 // the safe area is sufficiently respected, when safe area is on the right side.
175 // e.g. Android device with system navigation bar in landscape mode.
176 final Finder rightPaddedCenterItem = find.byKey(const Key('Center'));
177 final Offset rightPaddedCenter = tester.getCenter(rightPaddedCenterItem);
178 expect(
179 rightPaddedCenter.dx,
180 closeTo((defaultWidth - safeAreaPadding) / 2, precisionErrorTolerance),
181 );
182
183 await tester.pumpWidget(
184 _buildWidget(
185 MediaQuery(
186 data: const MediaQueryData(
187 padding: EdgeInsets.fromLTRB(safeAreaPadding, 0, safeAreaPadding, safeAreaPadding),
188 ),
189 child: navigationBar(),
190 ),
191 ),
192 );
193
194 // The position of center item of navigation bar should indicate whether
195 // the safe area is sufficiently respected, when safe areas are on both sides.
196 // e.g. iOS device with both sides of round corner.
197 final Finder paddedCenterItem = find.byKey(const Key('Center'));
198 final Offset paddedCenter = tester.getCenter(paddedCenterItem);
199 expect(paddedCenter.dx, closeTo(defaultWidth / 2, precisionErrorTolerance));
200 });
201
202 testWidgets('Material2 - NavigationBar uses proper defaults when no parameters are given', (
203 WidgetTester tester,
204 ) async {
205 // M2 settings that were hand coded.
206 await tester.pumpWidget(
207 _buildWidget(
208 NavigationBar(
209 destinations: const <Widget>[
210 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
211 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
212 ],
213 onDestinationSelected: (int i) {},
214 ),
215 useMaterial3: false,
216 ),
217 );
218
219 expect(_getMaterial(tester).color, const Color(0xffeaeaea));
220 expect(_getMaterial(tester).surfaceTintColor, null);
221 expect(_getMaterial(tester).elevation, 0);
222 expect(tester.getSize(find.byType(NavigationBar)).height, 80);
223 expect(_getIndicatorDecoration(tester)?.color, const Color(0x3d2196f3));
224 expect(
225 _getIndicatorDecoration(tester)?.shape,
226 RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
227 );
228 });
229
230 testWidgets('Material3 - NavigationBar uses proper defaults when no parameters are given', (
231 WidgetTester tester,
232 ) async {
233 // M3 settings from the token database.
234 final ThemeData theme = ThemeData();
235 await tester.pumpWidget(
236 _buildWidget(
237 NavigationBar(
238 destinations: const <Widget>[
239 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
240 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
241 ],
242 onDestinationSelected: (int i) {},
243 ),
244 useMaterial3: theme.useMaterial3,
245 ),
246 );
247
248 expect(_getMaterial(tester).color, theme.colorScheme.surfaceContainer);
249 expect(_getMaterial(tester).surfaceTintColor, Colors.transparent);
250 expect(_getMaterial(tester).elevation, 3);
251 expect(tester.getSize(find.byType(NavigationBar)).height, 80);
252 expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
253 expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
254 });
255
256 testWidgets('Material2 - NavigationBar shows tooltips with text scaling', (
257 WidgetTester tester,
258 ) async {
259 const String label = 'A';
260
261 Widget buildApp({required TextScaler textScaler}) {
262 return MediaQuery(
263 data: MediaQueryData(textScaler: textScaler),
264 child: Localizations(
265 locale: const Locale('en', 'US'),
266 delegates: const <LocalizationsDelegate<dynamic>>[
267 DefaultMaterialLocalizations.delegate,
268 DefaultWidgetsLocalizations.delegate,
269 ],
270 child: MaterialApp(
271 theme: ThemeData(useMaterial3: false),
272 home: Navigator(
273 onGenerateRoute: (RouteSettings settings) {
274 return MaterialPageRoute<void>(
275 builder: (BuildContext context) {
276 return Scaffold(
277 bottomNavigationBar: NavigationBar(
278 destinations: const <NavigationDestination>[
279 NavigationDestination(
280 label: label,
281 icon: Icon(Icons.ac_unit),
282 tooltip: label,
283 ),
284 NavigationDestination(label: 'B', icon: Icon(Icons.battery_alert)),
285 ],
286 ),
287 );
288 },
289 );
290 },
291 ),
292 ),
293 ),
294 );
295 }
296
297 await tester.pumpWidget(buildApp(textScaler: TextScaler.noScaling));
298 expect(find.text(label), findsOneWidget);
299 await tester.longPress(find.text(label));
300 expect(find.text(label), findsNWidgets(2));
301
302 // The default size of a tooltip with the text A.
303 const Size defaultTooltipSize = Size(14.0, 14.0);
304 expect(tester.getSize(find.text(label).last), defaultTooltipSize);
305 // The duration is needed to ensure the tooltip disappears.
306 await tester.pumpAndSettle(const Duration(seconds: 2));
307
308 await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(4.0)));
309 expect(find.text(label), findsOneWidget);
310 await tester.longPress(find.text(label));
311 expect(
312 tester.getSize(find.text(label).last),
313 Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4),
314 );
315 });
316
317 testWidgets('Material3 - NavigationBar shows tooltips with text scaling', (
318 WidgetTester tester,
319 ) async {
320 const String label = 'A';
321
322 Widget buildApp({required TextScaler textScaler}) {
323 return MediaQuery(
324 data: MediaQueryData(textScaler: textScaler),
325 child: Localizations(
326 locale: const Locale('en', 'US'),
327 delegates: const <LocalizationsDelegate<dynamic>>[
328 DefaultMaterialLocalizations.delegate,
329 DefaultWidgetsLocalizations.delegate,
330 ],
331 child: MaterialApp(
332 home: Navigator(
333 onGenerateRoute: (RouteSettings settings) {
334 return MaterialPageRoute<void>(
335 builder: (BuildContext context) {
336 return Scaffold(
337 bottomNavigationBar: NavigationBar(
338 destinations: const <NavigationDestination>[
339 NavigationDestination(
340 label: label,
341 icon: Icon(Icons.ac_unit),
342 tooltip: label,
343 ),
344 NavigationDestination(label: 'B', icon: Icon(Icons.battery_alert)),
345 ],
346 ),
347 );
348 },
349 );
350 },
351 ),
352 ),
353 ),
354 );
355 }
356
357 await tester.pumpWidget(buildApp(textScaler: TextScaler.noScaling));
358 expect(find.text(label), findsOneWidget);
359 await tester.longPress(find.text(label));
360 expect(find.text(label), findsNWidgets(2));
361
362 expect(tester.getSize(find.text(label).last), const Size(14.25, 20.0));
363 // The duration is needed to ensure the tooltip disappears.
364 await tester.pumpAndSettle(const Duration(seconds: 2));
365
366 await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(4.0)));
367 expect(find.text(label), findsOneWidget);
368 await tester.longPress(find.text(label));
369
370 expect(tester.getSize(find.text(label).last), const Size(56.25, 80.0));
371 });
372
373 testWidgets('Material3 - NavigationBar label can scale and has maxScaleFactor', (
374 WidgetTester tester,
375 ) async {
376 const String label = 'A';
377
378 Widget buildApp({required TextScaler textScaler}) {
379 return MediaQuery(
380 data: MediaQueryData(textScaler: textScaler),
381 child: Localizations(
382 locale: const Locale('en', 'US'),
383 delegates: const <LocalizationsDelegate<dynamic>>[
384 DefaultMaterialLocalizations.delegate,
385 DefaultWidgetsLocalizations.delegate,
386 ],
387 child: MaterialApp(
388 home: Navigator(
389 onGenerateRoute: (RouteSettings settings) {
390 return MaterialPageRoute<void>(
391 builder: (BuildContext context) {
392 return Scaffold(
393 bottomNavigationBar: NavigationBar(
394 destinations: const <NavigationDestination>[
395 NavigationDestination(label: label, icon: Icon(Icons.ac_unit)),
396 NavigationDestination(label: 'B', icon: Icon(Icons.battery_alert)),
397 ],
398 ),
399 );
400 },
401 );
402 },
403 ),
404 ),
405 ),
406 );
407 }
408
409 await tester.pumpWidget(buildApp(textScaler: TextScaler.noScaling));
410 expect(find.text(label), findsOneWidget);
411 expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(12.5, 16.0)), true);
412
413 await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(1.1)));
414 await tester.pumpAndSettle();
415
416 expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(13.7, 18.0)), true);
417
418 await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(1.3)));
419
420 expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(16.1, 21.0)), true);
421
422 await tester.pumpWidget(buildApp(textScaler: const TextScaler.linear(4)));
423 expect(_sizeAlmostEqual(tester.getSize(find.text(label)), const Size(16.1, 21.0)), true);
424 });
425
426 testWidgets('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async {
427 await tester.pumpWidget(
428 MaterialApp(
429 home: Scaffold(
430 bottomNavigationBar: NavigationBar(
431 destinations: const <NavigationDestination>[
432 NavigationDestination(label: 'A', tooltip: 'A tooltip', icon: Icon(Icons.ac_unit)),
433 NavigationDestination(label: 'B', icon: Icon(Icons.battery_alert)),
434 NavigationDestination(label: 'C', icon: Icon(Icons.cake), tooltip: ''),
435 ],
436 ),
437 ),
438 ),
439 );
440
441 expect(find.text('A'), findsOneWidget);
442 await tester.longPress(find.text('A'));
443 expect(find.byTooltip('A tooltip'), findsOneWidget);
444
445 expect(find.text('B'), findsOneWidget);
446 await tester.longPress(find.text('B'));
447 expect(find.byTooltip('B'), findsOneWidget);
448
449 expect(find.text('C'), findsOneWidget);
450 await tester.longPress(find.text('C'));
451 expect(find.byTooltip('C'), findsNothing);
452 });
453
454 testWidgets('Navigation bar semantics', (WidgetTester tester) async {
455 Widget widget({int selectedIndex = 0}) {
456 return _buildWidget(
457 NavigationBar(
458 selectedIndex: selectedIndex,
459 destinations: const <Widget>[
460 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
461 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
462 NavigationDestination(icon: Icon(Icons.abc), label: 'ABC'),
463 ],
464 ),
465 );
466 }
467
468 await tester.pumpWidget(widget());
469
470 expect(
471 tester.getSemantics(find.text('AC')),
472 matchesSemantics(
473 label: 'AC${kIsWeb ? '' : '\nTab 1 of 3'}',
474 textDirection: TextDirection.ltr,
475 isFocusable: true,
476 isSelected: true,
477 isButton: true,
478 hasSelectedState: true,
479 hasEnabledState: true,
480 isEnabled: true,
481 hasTapAction: true,
482 hasFocusAction: true,
483 ),
484 );
485 expect(
486 tester.getSemantics(find.text('Alarm')),
487 matchesSemantics(
488 label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 3'}',
489 textDirection: TextDirection.ltr,
490 isFocusable: true,
491 isButton: true,
492 hasSelectedState: true,
493 hasEnabledState: true,
494 isEnabled: true,
495 hasTapAction: true,
496 hasFocusAction: true,
497 ),
498 );
499 expect(
500 tester.getSemantics(find.text('ABC')),
501 matchesSemantics(
502 label: 'ABC${kIsWeb ? '' : '\nTab 3 of 3'}',
503 textDirection: TextDirection.ltr,
504 isFocusable: true,
505 isButton: true,
506 hasSelectedState: true,
507 hasEnabledState: true,
508 isEnabled: true,
509 hasTapAction: true,
510 hasFocusAction: true,
511 ),
512 );
513
514 await tester.pumpWidget(widget(selectedIndex: 1));
515
516 expect(
517 tester.getSemantics(find.text('AC')),
518 matchesSemantics(
519 label: 'AC${kIsWeb ? '' : '\nTab 1 of 3'}',
520 textDirection: TextDirection.ltr,
521 isFocusable: true,
522 isButton: true,
523 hasEnabledState: true,
524 hasSelectedState: true,
525 isEnabled: true,
526 hasTapAction: true,
527 hasFocusAction: true,
528 ),
529 );
530 expect(
531 tester.getSemantics(find.text('Alarm')),
532 matchesSemantics(
533 label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 3'}',
534 textDirection: TextDirection.ltr,
535 isFocusable: true,
536 isSelected: true,
537 isButton: true,
538 hasEnabledState: true,
539 hasSelectedState: true,
540 isEnabled: true,
541 hasTapAction: true,
542 hasFocusAction: true,
543 ),
544 );
545 expect(
546 tester.getSemantics(find.text('ABC')),
547 matchesSemantics(
548 label: 'ABC${kIsWeb ? '' : '\nTab 3 of 3'}',
549 textDirection: TextDirection.ltr,
550 isFocusable: true,
551 isButton: true,
552 hasEnabledState: true,
553 hasSelectedState: true,
554 isEnabled: true,
555 hasTapAction: true,
556 hasFocusAction: true,
557 ),
558 );
559 });
560 testWidgets('Navigation bar disabled semantics', (WidgetTester tester) async {
561 Widget widget({int selectedIndex = 0}) {
562 return _buildWidget(
563 NavigationBar(
564 selectedIndex: selectedIndex,
565 destinations: const <Widget>[
566 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC', enabled: false),
567 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'Another'),
568 ],
569 ),
570 );
571 }
572
573 await tester.pumpWidget(widget());
574
575 expect(
576 tester.getSemantics(find.text('AC')),
577 matchesSemantics(
578 label: 'AC${kIsWeb ? '' : '\nTab 1 of 2'}',
579 textDirection: TextDirection.ltr,
580 isSelected: true,
581 hasSelectedState: true,
582 hasEnabledState: true,
583 isButton: true,
584 ),
585 );
586 });
587
588 testWidgets('Navigation bar semantics with some labels hidden', (WidgetTester tester) async {
589 Widget widget({int selectedIndex = 0}) {
590 return _buildWidget(
591 NavigationBar(
592 labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
593 selectedIndex: selectedIndex,
594 destinations: const <Widget>[
595 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
596 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
597 ],
598 ),
599 );
600 }
601
602 await tester.pumpWidget(widget());
603
604 expect(
605 tester.getSemantics(find.text('AC')),
606 matchesSemantics(
607 label: 'AC${kIsWeb ? '' : '\nTab 1 of 2'}',
608 textDirection: TextDirection.ltr,
609 isFocusable: true,
610 isSelected: true,
611 isButton: true,
612 hasEnabledState: true,
613 hasSelectedState: true,
614 isEnabled: true,
615 hasTapAction: true,
616 hasFocusAction: true,
617 ),
618 );
619 expect(
620 tester.getSemantics(find.text('Alarm')),
621 matchesSemantics(
622 label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 2'}',
623 textDirection: TextDirection.ltr,
624 isFocusable: true,
625 isButton: true,
626 hasEnabledState: true,
627 hasSelectedState: true,
628 isEnabled: true,
629 hasTapAction: true,
630 hasFocusAction: true,
631 ),
632 );
633
634 await tester.pumpWidget(widget(selectedIndex: 1));
635
636 expect(
637 tester.getSemantics(find.text('AC')),
638 matchesSemantics(
639 label: 'AC${kIsWeb ? '' : '\nTab 1 of 2'}',
640 textDirection: TextDirection.ltr,
641 isFocusable: true,
642 isButton: true,
643 hasEnabledState: true,
644 hasSelectedState: true,
645 isEnabled: true,
646 hasTapAction: true,
647 hasFocusAction: true,
648 ),
649 );
650 expect(
651 tester.getSemantics(find.text('Alarm')),
652 matchesSemantics(
653 label: 'Alarm${kIsWeb ? '' : '\nTab 2 of 2'}',
654 textDirection: TextDirection.ltr,
655 isFocusable: true,
656 hasEnabledState: true,
657 hasSelectedState: true,
658 isEnabled: true,
659 isSelected: true,
660 isButton: true,
661 hasTapAction: true,
662 hasFocusAction: true,
663 ),
664 );
665 });
666
667 testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
668 const int animationMilliseconds = 800;
669
670 Widget widget({TextScaler textScaler = TextScaler.noScaling}) {
671 return _buildWidget(
672 MediaQuery(
673 data: MediaQueryData(textScaler: textScaler),
674 child: NavigationBar(
675 animationDuration: const Duration(milliseconds: animationMilliseconds),
676 destinations: const <NavigationDestination>[
677 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
678 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
679 ],
680 ),
681 ),
682 );
683 }
684
685 await tester.pumpWidget(widget());
686 final double initialHeight = tester.getSize(find.byType(NavigationBar)).height;
687
688 await tester.pumpWidget(widget(textScaler: const TextScaler.linear(2)));
689 final double newHeight = tester.getSize(find.byType(NavigationBar)).height;
690
691 expect(newHeight, equals(initialHeight));
692 });
693
694 testWidgets('Material3 - Navigation indicator renders ripple', (WidgetTester tester) async {
695 // This is a regression test for https://github.com/flutter/flutter/issues/116751.
696 int selectedIndex = 0;
697
698 Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
699 return MaterialApp(
700 home: Scaffold(
701 bottomNavigationBar: Center(
702 child: NavigationBar(
703 selectedIndex: selectedIndex,
704 labelBehavior: labelBehavior,
705 destinations: const <Widget>[
706 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
707 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
708 ],
709 onDestinationSelected: (int i) {},
710 ),
711 ),
712 ),
713 );
714 }
715
716 await tester.pumpWidget(buildWidget());
717
718 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
719 await gesture.addPointer();
720 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
721 await tester.pumpAndSettle();
722
723 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
724 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
725 );
726 Offset indicatorCenter = const Offset(600, 30);
727 const Size includedIndicatorSize = Size(64, 32);
728 const Size excludedIndicatorSize = Size(74, 40);
729
730 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
731 expect(
732 inkFeatures,
733 paints
734 ..clipPath(
735 pathMatcher: isPathThat(
736 includes: <Offset>[
737 // Left center.
738 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
739 // Top center.
740 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
741 // Right center.
742 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
743 // Bottom center.
744 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
745 ],
746 excludes: <Offset>[
747 // Left center.
748 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
749 // Top center.
750 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
751 // Right center.
752 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
753 // Bottom center.
754 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
755 ],
756 ),
757 )
758 ..circle(
759 x: indicatorCenter.dx,
760 y: indicatorCenter.dy,
761 radius: 35.0,
762 color: const Color(0x0a000000),
763 ),
764 );
765
766 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
767 await tester.pumpWidget(
768 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
769 );
770 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
771 await tester.pumpAndSettle();
772
773 indicatorCenter = const Offset(600, 40);
774
775 expect(
776 inkFeatures,
777 paints
778 ..clipPath(
779 pathMatcher: isPathThat(
780 includes: <Offset>[
781 // Left center.
782 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
783 // Top center.
784 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
785 // Right center.
786 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
787 // Bottom center.
788 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
789 ],
790 excludes: <Offset>[
791 // Left center.
792 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
793 // Top center.
794 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
795 // Right center.
796 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
797 // Bottom center.
798 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
799 ],
800 ),
801 )
802 ..circle(
803 x: indicatorCenter.dx,
804 y: indicatorCenter.dy,
805 radius: 35.0,
806 color: const Color(0x0a000000),
807 ),
808 );
809
810 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
811 await tester.pumpWidget(
812 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
813 );
814 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
815 await tester.pumpAndSettle();
816
817 expect(
818 inkFeatures,
819 paints
820 ..clipPath(
821 pathMatcher: isPathThat(
822 includes: <Offset>[
823 // Left center.
824 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
825 // Top center.
826 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
827 // Right center.
828 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
829 // Bottom center.
830 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
831 ],
832 excludes: <Offset>[
833 // Left center.
834 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
835 // Top center.
836 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
837 // Right center.
838 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
839 // Bottom center.
840 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
841 ],
842 ),
843 )
844 ..circle(
845 x: indicatorCenter.dx,
846 y: indicatorCenter.dy,
847 radius: 35.0,
848 color: const Color(0x0a000000),
849 ),
850 );
851
852 // Make sure ripple is shifted when selectedIndex changes.
853 selectedIndex = 1;
854 await tester.pumpWidget(
855 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
856 );
857 await tester.pumpAndSettle();
858 indicatorCenter = const Offset(600, 30);
859
860 expect(
861 inkFeatures,
862 paints
863 ..clipPath(
864 pathMatcher: isPathThat(
865 includes: <Offset>[
866 // Left center.
867 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
868 // Top center.
869 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
870 // Right center.
871 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
872 // Bottom center.
873 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
874 ],
875 excludes: <Offset>[
876 // Left center.
877 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
878 // Top center.
879 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
880 // Right center.
881 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
882 // Bottom center.
883 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
884 ],
885 ),
886 )
887 ..circle(
888 x: indicatorCenter.dx,
889 y: indicatorCenter.dy,
890 radius: 35.0,
891 color: const Color(0x0a000000),
892 ),
893 );
894 });
895
896 testWidgets('Material3 - Navigation indicator ripple golden test', (WidgetTester tester) async {
897 // This is a regression test for https://github.com/flutter/flutter/issues/117420.
898
899 Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
900 return MaterialApp(
901 home: Scaffold(
902 bottomNavigationBar: Center(
903 child: NavigationBar(
904 labelBehavior: labelBehavior,
905 destinations: const <Widget>[
906 NavigationDestination(icon: SizedBox(), label: 'AC'),
907 NavigationDestination(icon: SizedBox(), label: 'Alarm'),
908 ],
909 onDestinationSelected: (int i) {},
910 ),
911 ),
912 ),
913 );
914 }
915
916 await tester.pumpWidget(buildWidget());
917
918 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
919 await gesture.addPointer();
920 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
921 await tester.pumpAndSettle();
922
923 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
924 await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysShow_m3.png'));
925
926 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
927 await tester.pumpWidget(
928 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
929 );
930 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
931 await tester.pumpAndSettle();
932
933 await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysHide_m3.png'));
934
935 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
936 await tester.pumpWidget(
937 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
938 );
939 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
940 await tester.pumpAndSettle();
941
942 await expectLater(
943 find.byType(NavigationBar),
944 matchesGoldenFile('indicator_onlyShowSelected_selected_m3.png'),
945 );
946
947 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
948 await tester.pumpAndSettle();
949
950 await expectLater(
951 find.byType(NavigationBar),
952 matchesGoldenFile('indicator_onlyShowSelected_unselected_m3.png'),
953 );
954 });
955
956 testWidgets('Navigation indicator scale transform', (WidgetTester tester) async {
957 int selectedIndex = 0;
958
959 Widget buildNavigationBar() {
960 return MaterialApp(
961 home: Scaffold(
962 bottomNavigationBar: Center(
963 child: NavigationBar(
964 selectedIndex: selectedIndex,
965 destinations: const <Widget>[
966 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
967 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
968 ],
969 onDestinationSelected: (int i) {},
970 ),
971 ),
972 ),
973 );
974 }
975
976 await tester.pumpWidget(buildNavigationBar());
977 await tester.pumpAndSettle();
978 final Finder transformFinder =
979 find
980 .descendant(of: find.byType(NavigationIndicator), matching: find.byType(Transform))
981 .last;
982 Matrix4 transform = tester.widget<Transform>(transformFinder).transform;
983 expect(transform.getColumn(0)[0], 0.0);
984
985 selectedIndex = 1;
986 await tester.pumpWidget(buildNavigationBar());
987 await tester.pump(const Duration(milliseconds: 100));
988 transform = tester.widget<Transform>(transformFinder).transform;
989 expect(transform.getColumn(0)[0], closeTo(0.7805849514007568, precisionErrorTolerance));
990
991 await tester.pump(const Duration(milliseconds: 100));
992 transform = tester.widget<Transform>(transformFinder).transform;
993 expect(transform.getColumn(0)[0], closeTo(0.9473570239543915, precisionErrorTolerance));
994
995 await tester.pumpAndSettle();
996 transform = tester.widget<Transform>(transformFinder).transform;
997 expect(transform.getColumn(0)[0], 1.0);
998 });
999
1000 testWidgets('Material3 - Navigation destination updates indicator color and shape', (
1001 WidgetTester tester,
1002 ) async {
1003 final ThemeData theme = ThemeData();
1004 const Color color = Color(0xff0000ff);
1005 const ShapeBorder shape = RoundedRectangleBorder();
1006
1007 Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
1008 return MaterialApp(
1009 theme: theme,
1010 home: Scaffold(
1011 bottomNavigationBar: RepaintBoundary(
1012 child: NavigationBar(
1013 indicatorColor: indicatorColor,
1014 indicatorShape: indicatorShape,
1015 destinations: const <Widget>[
1016 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1017 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1018 ],
1019 onDestinationSelected: (int i) {},
1020 ),
1021 ),
1022 ),
1023 );
1024 }
1025
1026 await tester.pumpWidget(buildNavigationBar());
1027
1028 // Test default indicator color and shape.
1029 expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
1030 expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
1031
1032 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1033 await gesture.addPointer();
1034 await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
1035 await tester.pumpAndSettle();
1036
1037 // Test default indicator color and shape with ripple.
1038 await expectLater(
1039 find.byType(NavigationBar),
1040 matchesGoldenFile('m3.navigation_bar.default.indicator.inkwell.shape.png'),
1041 );
1042
1043 await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
1044
1045 // Test custom indicator color and shape.
1046 expect(_getIndicatorDecoration(tester)?.color, color);
1047 expect(_getIndicatorDecoration(tester)?.shape, shape);
1048
1049 // Test custom indicator color and shape with ripple.
1050 await expectLater(
1051 find.byType(NavigationBar),
1052 matchesGoldenFile('m3.navigation_bar.custom.indicator.inkwell.shape.png'),
1053 );
1054 });
1055
1056 testWidgets('Destinations respect their disabled state', (WidgetTester tester) async {
1057 int selectedIndex = 0;
1058
1059 await tester.pumpWidget(
1060 _buildWidget(
1061 NavigationBar(
1062 destinations: const <Widget>[
1063 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1064 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1065 NavigationDestination(icon: Icon(Icons.bookmark), label: 'Bookmark', enabled: false),
1066 ],
1067 onDestinationSelected: (int i) => selectedIndex = i,
1068 selectedIndex: selectedIndex,
1069 ),
1070 ),
1071 );
1072
1073 await tester.tap(find.text('AC'));
1074 expect(selectedIndex, 0);
1075
1076 await tester.tap(find.text('Alarm'));
1077 expect(selectedIndex, 1);
1078
1079 await tester.tap(find.text('Bookmark'));
1080 expect(selectedIndex, 1);
1081 });
1082
1083 testWidgets('NavigationBar respects overlayColor in active/pressed/hovered states', (
1084 WidgetTester tester,
1085 ) async {
1086 tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
1087 const Color hoverColor = Color(0xff0000ff);
1088 const Color focusColor = Color(0xff00ffff);
1089 const Color pressedColor = Color(0xffff00ff);
1090 final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>((
1091 Set<MaterialState> states,
1092 ) {
1093 if (states.contains(MaterialState.hovered)) {
1094 return hoverColor;
1095 }
1096 if (states.contains(MaterialState.focused)) {
1097 return focusColor;
1098 }
1099 if (states.contains(MaterialState.pressed)) {
1100 return pressedColor;
1101 }
1102 return Colors.transparent;
1103 });
1104
1105 await tester.pumpWidget(
1106 MaterialApp(
1107 home: Scaffold(
1108 bottomNavigationBar: RepaintBoundary(
1109 child: NavigationBar(
1110 overlayColor: overlayColor,
1111 destinations: const <Widget>[
1112 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1113 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1114 ],
1115 onDestinationSelected: (int i) {},
1116 ),
1117 ),
1118 ),
1119 ),
1120 );
1121
1122 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1123 await gesture.addPointer();
1124 await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
1125 await tester.pumpAndSettle();
1126
1127 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
1128 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
1129 );
1130
1131 // Test hovered state.
1132 expect(
1133 inkFeatures,
1134 kIsWeb
1135 ? (paints
1136 ..rrect()
1137 ..rrect()
1138 ..circle(color: hoverColor))
1139 : (paints..circle(color: hoverColor)),
1140 );
1141
1142 await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
1143 await tester.pumpAndSettle();
1144
1145 // Test pressed state.
1146 expect(
1147 inkFeatures,
1148 kIsWeb
1149 ? (paints
1150 ..circle()
1151 ..circle()
1152 ..circle(color: pressedColor))
1153 : (paints
1154 ..circle()
1155 ..circle(color: pressedColor)),
1156 );
1157
1158 await gesture.up();
1159 await tester.pumpAndSettle();
1160
1161 // Press tab to focus the navigation bar.
1162 await tester.sendKeyEvent(LogicalKeyboardKey.tab);
1163 await tester.pumpAndSettle();
1164
1165 // Test focused state.
1166 expect(
1167 inkFeatures,
1168 kIsWeb
1169 ? (paints
1170 ..circle()
1171 ..circle(color: focusColor))
1172 : (paints
1173 ..circle()
1174 ..circle(color: focusColor)),
1175 );
1176 });
1177
1178 testWidgets('NavigationBar.labelPadding overrides NavigationDestination.label padding', (
1179 WidgetTester tester,
1180 ) async {
1181 const EdgeInsetsGeometry labelPadding = EdgeInsets.all(8);
1182 Widget buildNavigationBar({EdgeInsetsGeometry? labelPadding}) {
1183 return MaterialApp(
1184 home: Scaffold(
1185 bottomNavigationBar: NavigationBar(
1186 labelPadding: labelPadding,
1187 destinations: const <Widget>[
1188 NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
1189 NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
1190 ],
1191 onDestinationSelected: (int i) {},
1192 ),
1193 ),
1194 );
1195 }
1196
1197 await tester.pumpWidget(buildNavigationBar());
1198 expect(_getLabelPadding(tester, 'Home'), const EdgeInsets.only(top: 4));
1199 expect(_getLabelPadding(tester, 'Settings'), const EdgeInsets.only(top: 4));
1200
1201 await tester.pumpWidget(buildNavigationBar(labelPadding: labelPadding));
1202 expect(_getLabelPadding(tester, 'Home'), labelPadding);
1203 expect(_getLabelPadding(tester, 'Settings'), labelPadding);
1204 });
1205
1206 group('Material 2', () {
1207 // These tests are only relevant for Material 2. Once Material 2
1208 // support is deprecated and the APIs are removed, these tests
1209 // can be deleted.
1210
1211 testWidgets('Material2 - Navigation destination updates indicator color and shape', (
1212 WidgetTester tester,
1213 ) async {
1214 final ThemeData theme = ThemeData(useMaterial3: false);
1215 const Color color = Color(0xff0000ff);
1216 const ShapeBorder shape = RoundedRectangleBorder();
1217
1218 Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
1219 return MaterialApp(
1220 theme: theme,
1221 home: Scaffold(
1222 bottomNavigationBar: NavigationBar(
1223 indicatorColor: indicatorColor,
1224 indicatorShape: indicatorShape,
1225 destinations: const <Widget>[
1226 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1227 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1228 ],
1229 onDestinationSelected: (int i) {},
1230 ),
1231 ),
1232 );
1233 }
1234
1235 await tester.pumpWidget(buildNavigationBar());
1236
1237 // Test default indicator color and shape.
1238 expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
1239 expect(
1240 _getIndicatorDecoration(tester)?.shape,
1241 const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
1242 );
1243
1244 await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
1245
1246 // Test custom indicator color and shape.
1247 expect(_getIndicatorDecoration(tester)?.color, color);
1248 expect(_getIndicatorDecoration(tester)?.shape, shape);
1249 });
1250
1251 testWidgets('Material2 - Navigation indicator renders ripple', (WidgetTester tester) async {
1252 // This is a regression test for https://github.com/flutter/flutter/issues/116751.
1253 int selectedIndex = 0;
1254
1255 Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
1256 return MaterialApp(
1257 theme: ThemeData(useMaterial3: false),
1258 home: Scaffold(
1259 bottomNavigationBar: Center(
1260 child: NavigationBar(
1261 selectedIndex: selectedIndex,
1262 labelBehavior: labelBehavior,
1263 destinations: const <Widget>[
1264 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1265 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1266 ],
1267 onDestinationSelected: (int i) {},
1268 ),
1269 ),
1270 ),
1271 );
1272 }
1273
1274 await tester.pumpWidget(buildWidget());
1275
1276 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1277 await gesture.addPointer();
1278 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
1279 await tester.pumpAndSettle();
1280
1281 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
1282 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
1283 );
1284 Offset indicatorCenter = const Offset(600, 33);
1285 const Size includedIndicatorSize = Size(64, 32);
1286 const Size excludedIndicatorSize = Size(74, 40);
1287
1288 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
1289 expect(
1290 inkFeatures,
1291 paints
1292 ..clipPath(
1293 pathMatcher: isPathThat(
1294 includes: <Offset>[
1295 // Left center.
1296 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
1297 // Top center.
1298 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
1299 // Right center.
1300 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
1301 // Bottom center.
1302 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
1303 ],
1304 excludes: <Offset>[
1305 // Left center.
1306 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1307 // Top center.
1308 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
1309 // Right center.
1310 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1311 // Bottom center.
1312 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
1313 ],
1314 ),
1315 )
1316 ..circle(
1317 x: indicatorCenter.dx,
1318 y: indicatorCenter.dy,
1319 radius: 35.0,
1320 color: const Color(0x0a000000),
1321 ),
1322 );
1323
1324 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
1325 await tester.pumpWidget(
1326 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
1327 );
1328 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
1329 await tester.pumpAndSettle();
1330
1331 indicatorCenter = const Offset(600, 40);
1332
1333 expect(
1334 inkFeatures,
1335 paints
1336 ..clipPath(
1337 pathMatcher: isPathThat(
1338 includes: <Offset>[
1339 // Left center.
1340 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
1341 // Top center.
1342 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
1343 // Right center.
1344 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
1345 // Bottom center.
1346 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
1347 ],
1348 excludes: <Offset>[
1349 // Left center.
1350 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1351 // Top center.
1352 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
1353 // Right center.
1354 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1355 // Bottom center.
1356 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
1357 ],
1358 ),
1359 )
1360 ..circle(
1361 x: indicatorCenter.dx,
1362 y: indicatorCenter.dy,
1363 radius: 35.0,
1364 color: const Color(0x0a000000),
1365 ),
1366 );
1367
1368 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
1369 await tester.pumpWidget(
1370 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
1371 );
1372 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
1373 await tester.pumpAndSettle();
1374
1375 expect(
1376 inkFeatures,
1377 paints
1378 ..clipPath(
1379 pathMatcher: isPathThat(
1380 includes: <Offset>[
1381 // Left center.
1382 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
1383 // Top center.
1384 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
1385 // Right center.
1386 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
1387 // Bottom center.
1388 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
1389 ],
1390 excludes: <Offset>[
1391 // Left center.
1392 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1393 // Top center.
1394 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
1395 // Right center.
1396 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1397 // Bottom center.
1398 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
1399 ],
1400 ),
1401 )
1402 ..circle(
1403 x: indicatorCenter.dx,
1404 y: indicatorCenter.dy,
1405 radius: 35.0,
1406 color: const Color(0x0a000000),
1407 ),
1408 );
1409
1410 // Make sure ripple is shifted when selectedIndex changes.
1411 selectedIndex = 1;
1412 await tester.pumpWidget(
1413 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
1414 );
1415 await tester.pumpAndSettle();
1416 indicatorCenter = const Offset(600, 33);
1417
1418 expect(
1419 inkFeatures,
1420 paints
1421 ..clipPath(
1422 pathMatcher: isPathThat(
1423 includes: <Offset>[
1424 // Left center.
1425 Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
1426 // Top center.
1427 Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
1428 // Right center.
1429 Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
1430 // Bottom center.
1431 Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
1432 ],
1433 excludes: <Offset>[
1434 // Left center.
1435 Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1436 // Top center.
1437 Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
1438 // Right center.
1439 Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
1440 // Bottom center.
1441 Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
1442 ],
1443 ),
1444 )
1445 ..circle(
1446 x: indicatorCenter.dx,
1447 y: indicatorCenter.dy,
1448 radius: 35.0,
1449 color: const Color(0x0a000000),
1450 ),
1451 );
1452 });
1453
1454 testWidgets('Material2 - Navigation indicator ripple golden test', (WidgetTester tester) async {
1455 // This is a regression test for https://github.com/flutter/flutter/issues/117420.
1456
1457 Widget buildWidget({NavigationDestinationLabelBehavior? labelBehavior}) {
1458 return MaterialApp(
1459 theme: ThemeData(useMaterial3: false),
1460 home: Scaffold(
1461 bottomNavigationBar: Center(
1462 child: NavigationBar(
1463 labelBehavior: labelBehavior,
1464 destinations: const <Widget>[
1465 NavigationDestination(icon: SizedBox(), label: 'AC'),
1466 NavigationDestination(icon: SizedBox(), label: 'Alarm'),
1467 ],
1468 onDestinationSelected: (int i) {},
1469 ),
1470 ),
1471 ),
1472 );
1473 }
1474
1475 await tester.pumpWidget(buildWidget());
1476
1477 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1478 await gesture.addPointer();
1479 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
1480 await tester.pumpAndSettle();
1481
1482 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
1483 await expectLater(
1484 find.byType(NavigationBar),
1485 matchesGoldenFile('indicator_alwaysShow_m2.png'),
1486 );
1487
1488 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
1489 await tester.pumpWidget(
1490 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide),
1491 );
1492 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
1493 await tester.pumpAndSettle();
1494
1495 await expectLater(
1496 find.byType(NavigationBar),
1497 matchesGoldenFile('indicator_alwaysHide_m2.png'),
1498 );
1499
1500 // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
1501 await tester.pumpWidget(
1502 buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected),
1503 );
1504 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
1505 await tester.pumpAndSettle();
1506
1507 await expectLater(
1508 find.byType(NavigationBar),
1509 matchesGoldenFile('indicator_onlyShowSelected_selected_m2.png'),
1510 );
1511
1512 await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
1513 await tester.pumpAndSettle();
1514
1515 await expectLater(
1516 find.byType(NavigationBar),
1517 matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png'),
1518 );
1519 });
1520
1521 testWidgets('Destination icon does not rebuild when tapped', (WidgetTester tester) async {
1522 // This is a regression test for https://github.com/flutter/flutter/issues/122811.
1523
1524 Widget buildNavigationBar() {
1525 return MaterialApp(
1526 home: Scaffold(
1527 bottomNavigationBar: StatefulBuilder(
1528 builder: (BuildContext context, StateSetter setState) {
1529 int selectedIndex = 0;
1530 return NavigationBar(
1531 selectedIndex: selectedIndex,
1532 destinations: const <Widget>[
1533 NavigationDestination(
1534 icon: IconWithRandomColor(icon: Icons.ac_unit),
1535 label: 'AC',
1536 ),
1537 NavigationDestination(
1538 icon: IconWithRandomColor(icon: Icons.access_alarm),
1539 label: 'Alarm',
1540 ),
1541 ],
1542 onDestinationSelected: (int i) {
1543 setState(() {
1544 selectedIndex = i;
1545 });
1546 },
1547 );
1548 },
1549 ),
1550 ),
1551 );
1552 }
1553
1554 await tester.pumpWidget(buildNavigationBar());
1555 Icon icon = tester.widget<Icon>(find.byType(Icon).last);
1556 final Color initialColor = icon.color!;
1557
1558 // Trigger a rebuild.
1559 await tester.tap(find.text('Alarm'));
1560 await tester.pumpAndSettle();
1561
1562 // Icon color should be the same as before the rebuild.
1563 icon = tester.widget<Icon>(find.byType(Icon).last);
1564 expect(icon.color, initialColor);
1565 });
1566 });
1567
1568 testWidgets('NavigationBar.labelPadding overrides NavigationDestination.label padding', (
1569 WidgetTester tester,
1570 ) async {
1571 const String selectedText = 'Home';
1572 const String unselectedText = 'Settings';
1573 const EdgeInsetsGeometry labelPadding = EdgeInsets.all(8);
1574 Widget buildNavigationBar({EdgeInsetsGeometry? labelPadding}) {
1575 return MaterialApp(
1576 home: Scaffold(
1577 bottomNavigationBar: NavigationBar(
1578 labelPadding: labelPadding,
1579 destinations: const <Widget>[
1580 NavigationDestination(icon: Icon(Icons.home), label: selectedText),
1581 NavigationDestination(icon: Icon(Icons.settings), label: unselectedText),
1582 ],
1583 onDestinationSelected: (int i) {},
1584 ),
1585 ),
1586 );
1587 }
1588
1589 await tester.pumpWidget(buildNavigationBar());
1590 expect(_getLabelPadding(tester, selectedText), const EdgeInsets.only(top: 4));
1591 expect(_getLabelPadding(tester, unselectedText), const EdgeInsets.only(top: 4));
1592
1593 await tester.pumpWidget(buildNavigationBar(labelPadding: labelPadding));
1594 expect(_getLabelPadding(tester, selectedText), labelPadding);
1595 expect(_getLabelPadding(tester, unselectedText), labelPadding);
1596 });
1597
1598 testWidgets('NavigationBar.labelTextStyle overrides NavigationDestination.label text style', (
1599 WidgetTester tester,
1600 ) async {
1601 const String selectedText = 'Home';
1602 const String unselectedText = 'Settings';
1603 const String disabledText = 'Bookmark';
1604 final ThemeData theme = ThemeData();
1605 Widget buildNavigationBar({WidgetStateProperty<TextStyle?>? labelTextStyle}) {
1606 return MaterialApp(
1607 theme: theme,
1608 home: Scaffold(
1609 bottomNavigationBar: NavigationBar(
1610 labelTextStyle: labelTextStyle,
1611 destinations: const <Widget>[
1612 NavigationDestination(icon: Icon(Icons.home), label: selectedText),
1613 NavigationDestination(icon: Icon(Icons.settings), label: unselectedText),
1614 NavigationDestination(
1615 enabled: false,
1616 icon: Icon(Icons.bookmark),
1617 label: disabledText,
1618 ),
1619 ],
1620 ),
1621 ),
1622 );
1623 }
1624
1625 await tester.pumpWidget(buildNavigationBar());
1626
1627 // Test selected label text style.
1628 expect(_getLabelStyle(tester, selectedText).fontSize, equals(12.0));
1629 expect(_getLabelStyle(tester, selectedText).color, equals(theme.colorScheme.onSurface));
1630
1631 // Test unselected label text style.
1632 expect(_getLabelStyle(tester, unselectedText).fontSize, equals(12.0));
1633 expect(
1634 _getLabelStyle(tester, unselectedText).color,
1635 equals(theme.colorScheme.onSurfaceVariant),
1636 );
1637
1638 // Test disabled label text style.
1639 expect(_getLabelStyle(tester, disabledText).fontSize, equals(12.0));
1640 expect(
1641 _getLabelStyle(tester, disabledText).color,
1642 equals(theme.colorScheme.onSurfaceVariant.withOpacity(0.38)),
1643 );
1644
1645 const TextStyle selectedTextStyle = TextStyle(fontSize: 15, color: Color(0xFF00FF00));
1646 const TextStyle unselectedTextStyle = TextStyle(fontSize: 15, color: Color(0xFF0000FF));
1647 const TextStyle disabledTextStyle = TextStyle(fontSize: 16, color: Color(0xFFFF0000));
1648 await tester.pumpWidget(
1649 buildNavigationBar(
1650 labelTextStyle:
1651 const WidgetStateProperty<TextStyle?>.fromMap(<WidgetStatesConstraint, TextStyle?>{
1652 WidgetState.disabled: disabledTextStyle,
1653 WidgetState.selected: selectedTextStyle,
1654 WidgetState.any: unselectedTextStyle,
1655 }),
1656 ),
1657 );
1658
1659 // Test selected label text style.
1660 expect(_getLabelStyle(tester, selectedText).fontSize, equals(selectedTextStyle.fontSize));
1661 expect(_getLabelStyle(tester, selectedText).color, equals(selectedTextStyle.color));
1662
1663 // Test unselected label text style.
1664 expect(_getLabelStyle(tester, unselectedText).fontSize, equals(unselectedTextStyle.fontSize));
1665 expect(_getLabelStyle(tester, unselectedText).color, equals(unselectedTextStyle.color));
1666
1667 // Test disabled label text style.
1668 expect(_getLabelStyle(tester, disabledText).fontSize, equals(disabledTextStyle.fontSize));
1669 expect(_getLabelStyle(tester, disabledText).color, equals(disabledTextStyle.color));
1670 });
1671
1672 testWidgets('NavigationBar.maintainBottomViewPadding can consume bottom MediaQuery.padding', (
1673 WidgetTester tester,
1674 ) async {
1675 const double bottomPadding = 40;
1676 const TextDirection textDirection = TextDirection.ltr;
1677
1678 await tester.pumpWidget(
1679 MaterialApp(
1680 home: Directionality(
1681 textDirection: textDirection,
1682 child: MediaQuery(
1683 data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
1684 child: Scaffold(
1685 bottomNavigationBar: NavigationBar(
1686 maintainBottomViewPadding: true,
1687 destinations: const <Widget>[
1688 NavigationDestination(icon: Icon(Icons.ac_unit), label: 'AC'),
1689 NavigationDestination(icon: Icon(Icons.access_alarm), label: 'Alarm'),
1690 ],
1691 ),
1692 ),
1693 ),
1694 ),
1695 ),
1696 );
1697
1698 final double safeAreaBottomPadding =
1699 tester.widget<Padding>(find.byType(Padding).first).padding.resolve(textDirection).bottom;
1700 expect(safeAreaBottomPadding, equals(0));
1701 });
1702}
1703
1704Widget _buildWidget(Widget child, {bool? useMaterial3}) {
1705 return MaterialApp(
1706 theme: ThemeData(useMaterial3: useMaterial3),
1707 home: Scaffold(bottomNavigationBar: Center(child: child)),
1708 );
1709}
1710
1711Material _getMaterial(WidgetTester tester) {
1712 return tester.firstWidget<Material>(
1713 find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
1714 );
1715}
1716
1717ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
1718 return tester
1719 .firstWidget<Ink>(
1720 find.descendant(of: find.byType(FadeTransition), matching: find.byType(Ink)),
1721 )
1722 .decoration
1723 as ShapeDecoration?;
1724}
1725
1726class IconWithRandomColor extends StatelessWidget {
1727 const IconWithRandomColor({super.key, required this.icon});
1728
1729 final IconData icon;
1730
1731 @override
1732 Widget build(BuildContext context) {
1733 final Color randomColor = Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
1734 return Icon(icon, color: randomColor);
1735 }
1736}
1737
1738bool _sizeAlmostEqual(Size a, Size b, {double maxDiff = 0.05}) {
1739 return (a.width - b.width).abs() <= maxDiff && (a.height - b.height).abs() <= maxDiff;
1740}
1741
1742EdgeInsetsGeometry _getLabelPadding(WidgetTester tester, String text) {
1743 return tester
1744 .widget<Padding>(find.ancestor(of: find.text(text), matching: find.byType(Padding)).first)
1745 .padding;
1746}
1747
1748TextStyle _getLabelStyle(WidgetTester tester, String text) {
1749 return tester
1750 .widget<RichText>(find.descendant(of: find.text(text), matching: find.byType(RichText)))
1751 .text
1752 .style!;
1753}
1754

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com