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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter_test/flutter_test.dart';
10
11import '../widgets/semantics_tester.dart';
12
13void main() {
14 testWidgets('Custom selected and unselected textStyles are honored', (WidgetTester tester) async {
15 const TextStyle selectedTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 17.0);
16 const TextStyle unselectedTextStyle = TextStyle(fontWeight: FontWeight.w800, fontSize: 11.0);
17
18 await _pumpNavigationRail(
19 tester,
20 navigationRail: NavigationRail(
21 selectedIndex: 0,
22 destinations: _destinations(),
23 labelType: NavigationRailLabelType.all,
24 selectedLabelTextStyle: selectedTextStyle,
25 unselectedLabelTextStyle: unselectedTextStyle,
26 ),
27 );
28
29 final TextStyle actualSelectedTextStyle = tester
30 .renderObject<RenderParagraph>(find.text('Abc'))
31 .text
32 .style!;
33 final TextStyle actualUnselectedTextStyle = tester
34 .renderObject<RenderParagraph>(find.text('Def'))
35 .text
36 .style!;
37 expect(actualSelectedTextStyle.fontSize, equals(selectedTextStyle.fontSize));
38 expect(actualSelectedTextStyle.fontWeight, equals(selectedTextStyle.fontWeight));
39 expect(actualUnselectedTextStyle.fontSize, equals(actualUnselectedTextStyle.fontSize));
40 expect(actualUnselectedTextStyle.fontWeight, equals(actualUnselectedTextStyle.fontWeight));
41 });
42
43 testWidgets('Custom selected and unselected iconThemes are honored', (WidgetTester tester) async {
44 const IconThemeData selectedIconTheme = IconThemeData(size: 36, color: Color(0x00000001));
45 const IconThemeData unselectedIconTheme = IconThemeData(size: 18, color: Color(0x00000002));
46
47 await _pumpNavigationRail(
48 tester,
49 navigationRail: NavigationRail(
50 selectedIndex: 0,
51 destinations: _destinations(),
52 labelType: NavigationRailLabelType.all,
53 selectedIconTheme: selectedIconTheme,
54 unselectedIconTheme: unselectedIconTheme,
55 ),
56 );
57
58 final TextStyle actualSelectedIconTheme = _iconStyle(tester, Icons.favorite);
59 final TextStyle actualUnselectedIconTheme = _iconStyle(tester, Icons.bookmark_border);
60 expect(actualSelectedIconTheme.color, equals(selectedIconTheme.color));
61 expect(actualSelectedIconTheme.fontSize, equals(selectedIconTheme.size));
62 expect(actualUnselectedIconTheme.color, equals(unselectedIconTheme.color));
63 expect(actualUnselectedIconTheme.fontSize, equals(unselectedIconTheme.size));
64 });
65
66 testWidgets('No selected destination when selectedIndex is null', (WidgetTester tester) async {
67 await _pumpNavigationRail(
68 tester,
69 navigationRail: NavigationRail(selectedIndex: null, destinations: _destinations()),
70 );
71
72 final Iterable<Semantics> semantics = tester.widgetList<Semantics>(find.byType(Semantics));
73 expect(semantics.where((Semantics s) => s.properties.selected ?? false), isEmpty);
74 });
75
76 testWidgets('backgroundColor can be changed', (WidgetTester tester) async {
77 await _pumpNavigationRail(
78 tester,
79 navigationRail: NavigationRail(
80 selectedIndex: 0,
81 destinations: _destinations(),
82 labelType: NavigationRailLabelType.all,
83 ),
84 );
85
86 expect(
87 _railMaterial(tester).color,
88 equals(const Color(0xfffef7ff)),
89 ); // default surface color in M3 colorScheme
90
91 await _pumpNavigationRail(
92 tester,
93 navigationRail: NavigationRail(
94 selectedIndex: 0,
95 destinations: _destinations(),
96 labelType: NavigationRailLabelType.all,
97 backgroundColor: Colors.green,
98 ),
99 );
100
101 expect(_railMaterial(tester).color, equals(Colors.green));
102 });
103
104 testWidgets('elevation can be changed', (WidgetTester tester) async {
105 await _pumpNavigationRail(
106 tester,
107 navigationRail: NavigationRail(
108 selectedIndex: 0,
109 destinations: _destinations(),
110 labelType: NavigationRailLabelType.all,
111 ),
112 );
113
114 expect(_railMaterial(tester).elevation, equals(0));
115
116 await _pumpNavigationRail(
117 tester,
118 navigationRail: NavigationRail(
119 selectedIndex: 0,
120 destinations: _destinations(),
121 labelType: NavigationRailLabelType.all,
122 elevation: 7,
123 ),
124 );
125
126 expect(_railMaterial(tester).elevation, equals(7));
127 });
128
129 testWidgets('Renders at the correct default width - [labelType]=none (default)', (
130 WidgetTester tester,
131 ) async {
132 await _pumpNavigationRail(
133 tester,
134 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
135 );
136
137 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
138 expect(renderBox.size.width, 80.0);
139 });
140
141 testWidgets('Renders at the correct default width - [labelType]=selected', (
142 WidgetTester tester,
143 ) async {
144 await _pumpNavigationRail(
145 tester,
146 navigationRail: NavigationRail(
147 selectedIndex: 0,
148 labelType: NavigationRailLabelType.selected,
149 destinations: _destinations(),
150 ),
151 );
152
153 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
154 expect(renderBox.size.width, 80.0);
155 });
156
157 testWidgets('Renders at the correct default width - [labelType]=all', (
158 WidgetTester tester,
159 ) async {
160 await _pumpNavigationRail(
161 tester,
162 navigationRail: NavigationRail(
163 selectedIndex: 0,
164 labelType: NavigationRailLabelType.all,
165 destinations: _destinations(),
166 ),
167 );
168
169 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
170 expect(renderBox.size.width, 80.0);
171 });
172
173 testWidgets('Leading and trailing spacing is correct with 0~2 destinations', (
174 WidgetTester tester,
175 ) async {
176 // Padding at the top of the rail.
177 const double topPadding = 8.0;
178 // Padding at after the leading widget.
179 const double spacerPadding = 8.0;
180 // Width of a destination.
181 const double destinationWidth = 80.0;
182 // Height of a destination indicator with icon.
183 const double destinationHeight = 32.0;
184 // Space between destinations.
185 const double destinationSpacing = 12.0;
186 // Height of the leading and trailing widgets.
187 const double fabHeight = 56.0;
188
189 late StateSetter stateSetter;
190 List<NavigationRailDestination> destinations = const <NavigationRailDestination>[];
191 Widget? leadingWidget;
192 Widget? trailingWidget;
193
194 const Key leadingWidgetKey = Key('leadingWidget');
195 const Key trailingWidgetKey = Key('trailingWidget');
196
197 void matchExpect(RenderBox renderBox, double nextDestinationY) {
198 expect(
199 renderBox.localToGlobal(Offset.zero),
200 Offset(
201 (destinationWidth - renderBox.size.width) / 2.0,
202 nextDestinationY + (destinationHeight - renderBox.size.height) / 2.0,
203 ),
204 );
205 }
206
207 await tester.pumpWidget(
208 MaterialApp(
209 home: StatefulBuilder(
210 builder: (BuildContext context, StateSetter setState) {
211 stateSetter = setState;
212 return Scaffold(
213 body: Row(
214 children: <Widget>[
215 NavigationRail(
216 destinations: destinations,
217 selectedIndex: null,
218 leading: leadingWidget,
219 trailing: trailingWidget,
220 ),
221 const Expanded(child: Text('body')),
222 ],
223 ),
224 );
225 },
226 ),
227 ),
228 );
229
230 // empty destinations and leading widget
231 stateSetter(() {
232 destinations = const <NavigationRailDestination>[];
233 leadingWidget = FloatingActionButton(key: leadingWidgetKey, onPressed: () {});
234 trailingWidget = null;
235 });
236 await tester.pumpAndSettle();
237 RenderBox leadingWidgetRenderBox = tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey));
238 expect(leadingWidgetRenderBox.localToGlobal(Offset.zero), const Offset(0.0, topPadding));
239
240 // one destination and leading widget
241 stateSetter(() {
242 destinations = const <NavigationRailDestination>[
243 NavigationRailDestination(
244 icon: Icon(Icons.favorite_border),
245 selectedIcon: Icon(Icons.favorite),
246 label: Text('Abc'),
247 ),
248 ];
249 });
250 await tester.pumpAndSettle();
251 double nextDestinationY = topPadding;
252 leadingWidgetRenderBox = tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey));
253 expect(
254 leadingWidgetRenderBox.localToGlobal(Offset.zero),
255 Offset((destinationWidth - leadingWidgetRenderBox.size.width) / 2.0, nextDestinationY),
256 );
257
258 nextDestinationY += fabHeight + spacerPadding + destinationSpacing / 2;
259 RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite_border);
260 matchExpect(firstIconRenderBox, nextDestinationY);
261
262 // two destinations and leading widget
263 stateSetter(() {
264 destinations = const <NavigationRailDestination>[
265 NavigationRailDestination(
266 icon: Icon(Icons.favorite_border),
267 selectedIcon: Icon(Icons.favorite),
268 label: Text('Abc'),
269 ),
270 NavigationRailDestination(
271 icon: Icon(Icons.bookmark_border),
272 selectedIcon: Icon(Icons.bookmark),
273 label: Text('Longer Label'),
274 ),
275 ];
276 });
277 await tester.pumpAndSettle();
278 nextDestinationY = topPadding;
279 leadingWidgetRenderBox = tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey));
280 expect(
281 leadingWidgetRenderBox.localToGlobal(Offset.zero),
282 Offset((destinationWidth - leadingWidgetRenderBox.size.width) / 2.0, nextDestinationY),
283 );
284
285 nextDestinationY += fabHeight + spacerPadding + destinationSpacing / 2;
286 firstIconRenderBox = _iconRenderBox(tester, Icons.favorite_border);
287 matchExpect(firstIconRenderBox, nextDestinationY);
288
289 nextDestinationY += destinationHeight + destinationSpacing;
290 RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
291 matchExpect(secondIconRenderBox, nextDestinationY);
292
293 // empty destinations and trailing widget
294 stateSetter(() {
295 destinations = const <NavigationRailDestination>[];
296 leadingWidget = null;
297 trailingWidget = FloatingActionButton(key: trailingWidgetKey, onPressed: () {});
298 });
299 await tester.pumpAndSettle();
300 RenderBox trailingWidgetRenderBox = tester.renderObject<RenderBox>(
301 find.byKey(trailingWidgetKey),
302 );
303 expect(trailingWidgetRenderBox.localToGlobal(Offset.zero), const Offset(0.0, topPadding));
304
305 // one destination and trailing widget
306 stateSetter(() {
307 destinations = const <NavigationRailDestination>[
308 NavigationRailDestination(
309 icon: Icon(Icons.favorite_border),
310 selectedIcon: Icon(Icons.favorite),
311 label: Text('Abc'),
312 ),
313 ];
314 });
315 await tester.pumpAndSettle();
316 nextDestinationY = topPadding + destinationSpacing / 2;
317 firstIconRenderBox = _iconRenderBox(tester, Icons.favorite_border);
318 matchExpect(firstIconRenderBox, nextDestinationY);
319
320 nextDestinationY += destinationHeight + destinationSpacing / 2;
321 trailingWidgetRenderBox = tester.renderObject<RenderBox>(find.byKey(trailingWidgetKey));
322 expect(
323 trailingWidgetRenderBox.localToGlobal(Offset.zero),
324 Offset((destinationWidth - trailingWidgetRenderBox.size.width) / 2.0, nextDestinationY),
325 );
326
327 // two destinations and trailing widget
328 stateSetter(() {
329 destinations = const <NavigationRailDestination>[
330 NavigationRailDestination(
331 icon: Icon(Icons.favorite_border),
332 selectedIcon: Icon(Icons.favorite),
333 label: Text('Abc'),
334 ),
335 NavigationRailDestination(
336 icon: Icon(Icons.bookmark_border),
337 selectedIcon: Icon(Icons.bookmark),
338 label: Text('Longer Label'),
339 ),
340 ];
341 });
342 await tester.pumpAndSettle();
343 nextDestinationY = topPadding + destinationSpacing / 2;
344 firstIconRenderBox = _iconRenderBox(tester, Icons.favorite_border);
345 matchExpect(firstIconRenderBox, nextDestinationY);
346
347 nextDestinationY += destinationHeight + destinationSpacing;
348 secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
349 matchExpect(secondIconRenderBox, nextDestinationY);
350
351 nextDestinationY += destinationHeight + destinationSpacing / 2.0;
352 trailingWidgetRenderBox = tester.renderObject<RenderBox>(find.byKey(trailingWidgetKey));
353 expect(
354 trailingWidgetRenderBox.localToGlobal(Offset.zero),
355 Offset((destinationWidth - trailingWidgetRenderBox.size.width) / 2.0, nextDestinationY),
356 );
357 });
358
359 testWidgets('Change destinations and selectedIndex', (WidgetTester tester) async {
360 late StateSetter stateSetter;
361 int? selectedIndex;
362 List<NavigationRailDestination> destinations = const <NavigationRailDestination>[];
363
364 await tester.pumpWidget(
365 MaterialApp(
366 home: StatefulBuilder(
367 builder: (BuildContext context, StateSetter setState) {
368 stateSetter = setState;
369 return Scaffold(
370 body: Row(
371 children: <Widget>[
372 NavigationRail(selectedIndex: selectedIndex, destinations: destinations),
373 const Expanded(child: Text('body')),
374 ],
375 ),
376 );
377 },
378 ),
379 ),
380 );
381
382 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 0);
383 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, isNull);
384
385 stateSetter(() {
386 destinations = const <NavigationRailDestination>[
387 NavigationRailDestination(
388 icon: Icon(Icons.favorite_border),
389 selectedIcon: Icon(Icons.favorite),
390 label: Text('Abc'),
391 ),
392 NavigationRailDestination(
393 icon: Icon(Icons.bookmark_border),
394 selectedIcon: Icon(Icons.bookmark),
395 label: Text('Longer Label'),
396 ),
397 ];
398 });
399
400 await tester.pumpAndSettle();
401
402 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 2);
403 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, isNull);
404
405 stateSetter(() {
406 selectedIndex = 0;
407 });
408
409 await tester.pumpAndSettle();
410
411 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 2);
412 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, 0);
413
414 stateSetter(() {
415 destinations = const <NavigationRailDestination>[
416 NavigationRailDestination(
417 icon: Icon(Icons.favorite_border),
418 selectedIcon: Icon(Icons.favorite),
419 label: Text('Abc'),
420 ),
421 ];
422 });
423
424 await tester.pumpAndSettle();
425
426 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 1);
427 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, 0);
428 });
429
430 testWidgets('Renders wider for a destination with a long label - [labelType]=all', (
431 WidgetTester tester,
432 ) async {
433 await _pumpNavigationRail(
434 tester,
435 navigationRail: NavigationRail(
436 selectedIndex: 0,
437 labelType: NavigationRailLabelType.all,
438 destinations: const <NavigationRailDestination>[
439 NavigationRailDestination(
440 icon: Icon(Icons.favorite_border),
441 selectedIcon: Icon(Icons.favorite),
442 label: Text('Abc'),
443 ),
444 NavigationRailDestination(
445 icon: Icon(Icons.bookmark_border),
446 selectedIcon: Icon(Icons.bookmark),
447 label: Text('Longer Label'),
448 ),
449 ],
450 ),
451 );
452
453 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
454 // Total padding is 16 (8 on each side).
455 expect(renderBox.size.width, _labelRenderBox(tester, 'Longer Label').size.width + 16.0);
456 });
457
458 testWidgets('Renders only icons - [labelType]=none (default)', (WidgetTester tester) async {
459 await _pumpNavigationRail(
460 tester,
461 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
462 );
463
464 expect(find.byIcon(Icons.favorite), findsOneWidget);
465 expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
466 expect(find.byIcon(Icons.star_border), findsOneWidget);
467 expect(find.byIcon(Icons.hotel), findsOneWidget);
468
469 // When there are no labels, a 0 opacity label is still shown for semantics.
470 expect(_labelOpacity(tester, 'Abc'), 0);
471 expect(_labelOpacity(tester, 'Def'), 0);
472 expect(_labelOpacity(tester, 'Ghi'), 0);
473 expect(_labelOpacity(tester, 'Jkl'), 0);
474 });
475
476 testWidgets('Renders icons and labels - [labelType]=all', (WidgetTester tester) async {
477 await _pumpNavigationRail(
478 tester,
479 navigationRail: NavigationRail(
480 selectedIndex: 0,
481 destinations: _destinations(),
482 labelType: NavigationRailLabelType.all,
483 ),
484 );
485
486 expect(find.byIcon(Icons.favorite), findsOneWidget);
487 expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
488 expect(find.byIcon(Icons.star_border), findsOneWidget);
489 expect(find.byIcon(Icons.hotel), findsOneWidget);
490
491 expect(find.text('Abc'), findsOneWidget);
492 expect(find.text('Def'), findsOneWidget);
493 expect(find.text('Ghi'), findsOneWidget);
494 expect(find.text('Jkl'), findsOneWidget);
495
496 // When displaying all labels, there is no opacity.
497 expect(_opacityAboveLabel('Abc'), findsNothing);
498 expect(_opacityAboveLabel('Def'), findsNothing);
499 expect(_opacityAboveLabel('Ghi'), findsNothing);
500 expect(_opacityAboveLabel('Jkl'), findsNothing);
501 });
502
503 testWidgets('Renders icons and selected label - [labelType]=selected', (
504 WidgetTester tester,
505 ) async {
506 await _pumpNavigationRail(
507 tester,
508 navigationRail: NavigationRail(
509 selectedIndex: 0,
510 destinations: _destinations(),
511 labelType: NavigationRailLabelType.selected,
512 ),
513 );
514
515 expect(find.byIcon(Icons.favorite), findsOneWidget);
516 expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
517 expect(find.byIcon(Icons.star_border), findsOneWidget);
518 expect(find.byIcon(Icons.hotel), findsOneWidget);
519
520 // Only the selected label is visible.
521 expect(_labelOpacity(tester, 'Abc'), 1);
522 expect(_labelOpacity(tester, 'Def'), 0);
523 expect(_labelOpacity(tester, 'Ghi'), 0);
524 expect(_labelOpacity(tester, 'Jkl'), 0);
525 });
526
527 testWidgets(
528 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)',
529 (WidgetTester tester) async {
530 // Padding at the top of the rail.
531 const double topPadding = 8.0;
532 // Width of a destination.
533 const double destinationWidth = 80.0;
534 // Height of a destination indicator with icon.
535 const double destinationHeight = 32.0;
536 // Space between destinations.
537 const double destinationPadding = 12.0;
538
539 await _pumpNavigationRail(
540 tester,
541 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
542 );
543
544 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
545 expect(renderBox.size.width, destinationWidth);
546
547 // The first destination below the rail top by some padding.
548 double nextDestinationY = topPadding + destinationPadding / 2;
549 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
550 expect(
551 firstIconRenderBox.localToGlobal(Offset.zero),
552 equals(
553 Offset(
554 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
555 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
556 ),
557 ),
558 );
559
560 // The second destination is one height below the first destination.
561 nextDestinationY += destinationHeight + destinationPadding;
562 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
563 expect(
564 secondIconRenderBox.localToGlobal(Offset.zero),
565 equals(
566 Offset(
567 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
568 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
569 ),
570 ),
571 );
572
573 // The third destination is one height below the second destination.
574 nextDestinationY += destinationHeight + destinationPadding;
575 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
576 expect(
577 thirdIconRenderBox.localToGlobal(Offset.zero),
578 equals(
579 Offset(
580 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
581 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
582 ),
583 ),
584 );
585
586 // The fourth destination is one height below the third destination.
587 nextDestinationY += destinationHeight + destinationPadding;
588 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
589 expect(
590 fourthIconRenderBox.localToGlobal(Offset.zero),
591 equals(
592 Offset(
593 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
594 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
595 ),
596 ),
597 );
598 },
599 );
600
601 testWidgets(
602 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0',
603 (WidgetTester tester) async {
604 // Since the rail is icon only, its destinations should not be affected by
605 // textScaleFactor.
606
607 // Padding at the top of the rail.
608 const double topPadding = 8.0;
609 // Width of a destination.
610 const double destinationWidth = 80.0;
611 // Height of a destination indicator with icon.
612 const double destinationHeight = 32.0;
613 // Space between destinations.
614 const double destinationPadding = 12.0;
615
616 await _pumpNavigationRail(
617 tester,
618 textScaleFactor: 3.0,
619 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
620 );
621
622 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
623 expect(renderBox.size.width, destinationWidth);
624
625 // The first destination below the rail top by some padding.
626 double nextDestinationY = topPadding + destinationPadding / 2;
627 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
628 expect(
629 firstIconRenderBox.localToGlobal(Offset.zero),
630 equals(
631 Offset(
632 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
633 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
634 ),
635 ),
636 );
637
638 // The second destination is one height below the first destination.
639 nextDestinationY += destinationHeight + destinationPadding;
640 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
641 expect(
642 secondIconRenderBox.localToGlobal(Offset.zero),
643 equals(
644 Offset(
645 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
646 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
647 ),
648 ),
649 );
650
651 // The third destination is one height below the second destination.
652 nextDestinationY += destinationHeight + destinationPadding;
653 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
654 expect(
655 thirdIconRenderBox.localToGlobal(Offset.zero),
656 equals(
657 Offset(
658 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
659 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
660 ),
661 ),
662 );
663
664 // The fourth destination is one height below the third destination.
665 nextDestinationY += destinationHeight + destinationPadding;
666 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
667 expect(
668 fourthIconRenderBox.localToGlobal(Offset.zero),
669 equals(
670 Offset(
671 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
672 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
673 ),
674 ),
675 );
676 },
677 );
678
679 testWidgets(
680 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75',
681 (WidgetTester tester) async {
682 // Since the rail is icon only, its destinations should not be affected by
683 // textScaleFactor.
684
685 // Padding at the top of the rail.
686 const double topPadding = 8.0;
687 // Width of a destination.
688 const double destinationWidth = 80.0;
689 // Height of a destination indicator with icon.
690 const double destinationHeight = 32.0;
691 // Space between destinations.
692 const double destinationPadding = 12.0;
693
694 await _pumpNavigationRail(
695 tester,
696 textScaleFactor: 0.75,
697 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
698 );
699
700 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
701 expect(renderBox.size.width, destinationWidth);
702
703 // The first destination below the rail top by some padding.
704 double nextDestinationY = topPadding + destinationPadding / 2;
705 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
706 expect(
707 firstIconRenderBox.localToGlobal(Offset.zero),
708 equals(
709 Offset(
710 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
711 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
712 ),
713 ),
714 );
715
716 // The second destination is one height below the first destination.
717 nextDestinationY += destinationHeight + destinationPadding;
718 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
719 expect(
720 secondIconRenderBox.localToGlobal(Offset.zero),
721 equals(
722 Offset(
723 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
724 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
725 ),
726 ),
727 );
728
729 // The third destination is one height below the second destination.
730 nextDestinationY += destinationHeight + destinationPadding;
731 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
732 expect(
733 thirdIconRenderBox.localToGlobal(Offset.zero),
734 equals(
735 Offset(
736 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
737 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
738 ),
739 ),
740 );
741
742 // The fourth destination is one height below the third destination.
743 nextDestinationY += destinationHeight + destinationPadding;
744 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
745 expect(
746 fourthIconRenderBox.localToGlobal(Offset.zero),
747 equals(
748 Offset(
749 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
750 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
751 ),
752 ),
753 );
754 },
755 );
756
757 testWidgets(
758 'Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)',
759 (WidgetTester tester) async {
760 // Padding at the top of the rail.
761 const double topPadding = 8.0;
762 // Width of a destination.
763 const double destinationWidth = 80.0;
764 // Height of a destination indicator with icon.
765 const double destinationHeight = 32.0;
766 // Space between the indicator and label.
767 const double destinationLabelSpacing = 4.0;
768 // Height of the label.
769 const double labelHeight = 16.0;
770 // Height of a destination with both icon and label.
771 const double destinationHeightWithLabel =
772 destinationHeight + destinationLabelSpacing + labelHeight;
773 // Space between destinations.
774 const double destinationSpacing = 12.0;
775
776 await _pumpNavigationRail(
777 tester,
778 navigationRail: NavigationRail(
779 selectedIndex: 0,
780 destinations: _destinations(),
781 labelType: NavigationRailLabelType.selected,
782 ),
783 );
784
785 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
786 expect(renderBox.size.width, destinationWidth);
787
788 // The first destination is topPadding below the rail top.
789 double nextDestinationY = topPadding;
790 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
791 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
792 expect(
793 firstIconRenderBox.localToGlobal(Offset.zero),
794 equals(
795 Offset(
796 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
797 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
798 ),
799 ),
800 );
801 expect(
802 firstLabelRenderBox.localToGlobal(Offset.zero),
803 equals(
804 Offset(
805 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
806 nextDestinationY + destinationHeight + destinationLabelSpacing,
807 ),
808 ),
809 );
810
811 // The second destination is below the first with some spacing.
812 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
813 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
814 expect(
815 secondIconRenderBox.localToGlobal(Offset.zero),
816 equals(
817 Offset(
818 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
819 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
820 ),
821 ),
822 );
823
824 // The third destination is below the second with some spacing.
825 nextDestinationY += destinationHeight + destinationSpacing;
826 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
827 expect(
828 thirdIconRenderBox.localToGlobal(Offset.zero),
829 equals(
830 Offset(
831 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
832 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
833 ),
834 ),
835 );
836
837 // The fourth destination is below the third with some spacing.
838 nextDestinationY += destinationHeight + destinationSpacing;
839 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
840 expect(
841 fourthIconRenderBox.localToGlobal(Offset.zero),
842 equals(
843 Offset(
844 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
845 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
846 ),
847 ),
848 );
849 },
850 );
851
852 testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (
853 WidgetTester tester,
854 ) async {
855 // Padding at the top of the rail.
856 const double topPadding = 8.0;
857 // Width of a destination.
858 const double destinationWidth = 125.5;
859 // Height of a destination indicator with icon.
860 const double destinationHeight = 32.0;
861 // Space between the indicator and label.
862 const double destinationLabelSpacing = 4.0;
863 // Height of the label.
864 const double labelHeight = 16.0 * 3.0;
865 // Height of a destination with both icon and label.
866 const double destinationHeightWithLabel =
867 destinationHeight + destinationLabelSpacing + labelHeight;
868 // Space between destinations.
869 const double destinationSpacing = 12.0;
870
871 await _pumpNavigationRail(
872 tester,
873 textScaleFactor: 3.0,
874 navigationRail: NavigationRail(
875 selectedIndex: 0,
876 destinations: _destinations(),
877 labelType: NavigationRailLabelType.selected,
878 ),
879 );
880
881 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
882 expect(renderBox.size.width, destinationWidth);
883
884 // The first destination topPadding below the rail top.
885 double nextDestinationY = topPadding;
886 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
887 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
888 expect(
889 firstIconRenderBox.localToGlobal(Offset.zero),
890 equals(
891 Offset(
892 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
893 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
894 ),
895 ),
896 );
897 expect(
898 firstLabelRenderBox.localToGlobal(Offset.zero),
899 equals(
900 Offset(
901 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
902 nextDestinationY + destinationHeight + destinationLabelSpacing,
903 ),
904 ),
905 );
906
907 // The second destination is below the first with some spacing.
908 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
909 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
910 expect(
911 secondIconRenderBox.localToGlobal(Offset.zero),
912 equals(
913 Offset(
914 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
915 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
916 ),
917 ),
918 );
919
920 // The third destination is below the second with some spacing.
921 nextDestinationY += destinationHeight + destinationSpacing;
922 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
923 expect(
924 thirdIconRenderBox.localToGlobal(Offset.zero),
925 equals(
926 Offset(
927 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
928 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
929 ),
930 ),
931 );
932
933 // The fourth destination is below the third with some spacing.
934 nextDestinationY += destinationHeight + destinationSpacing;
935 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
936 expect(
937 fourthIconRenderBox.localToGlobal(Offset.zero),
938 equals(
939 Offset(
940 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
941 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
942 ),
943 ),
944 );
945 });
946
947 testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (
948 WidgetTester tester,
949 ) async {
950 // Padding at the top of the rail.
951 const double topPadding = 8.0;
952 // Width of a destination.
953 const double destinationWidth = 80.0;
954 // Height of a destination indicator with icon.
955 const double destinationHeight = 32.0;
956 // Space between the indicator and label.
957 const double destinationLabelSpacing = 4.0;
958 // Height of the label.
959 const double labelHeight = 16.0 * 0.75;
960 // Height of a destination with both icon and label.
961 const double destinationHeightWithLabel =
962 destinationHeight + destinationLabelSpacing + labelHeight;
963 // Space between destinations.
964 const double destinationSpacing = 12.0;
965
966 await _pumpNavigationRail(
967 tester,
968 textScaleFactor: 0.75,
969 navigationRail: NavigationRail(
970 selectedIndex: 0,
971 destinations: _destinations(),
972 labelType: NavigationRailLabelType.selected,
973 ),
974 );
975
976 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
977 expect(renderBox.size.width, destinationWidth);
978
979 // The first destination topPadding below the rail top.
980 double nextDestinationY = topPadding;
981 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
982 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
983 expect(
984 firstIconRenderBox.localToGlobal(Offset.zero),
985 equals(
986 Offset(
987 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
988 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
989 ),
990 ),
991 );
992 expect(
993 firstLabelRenderBox.localToGlobal(Offset.zero),
994 equals(
995 Offset(
996 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
997 nextDestinationY + destinationHeight + destinationLabelSpacing,
998 ),
999 ),
1000 );
1001
1002 // The second destination is below the first with some spacing.
1003 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1004 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1005 expect(
1006 secondIconRenderBox.localToGlobal(Offset.zero),
1007 equals(
1008 Offset(
1009 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1010 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1011 ),
1012 ),
1013 );
1014
1015 // The third destination is below the second with some spacing.
1016 nextDestinationY += destinationHeight + destinationSpacing;
1017 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1018 expect(
1019 thirdIconRenderBox.localToGlobal(Offset.zero),
1020 equals(
1021 Offset(
1022 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1023 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1024 ),
1025 ),
1026 );
1027
1028 // The fourth destination is below the third with some spacing.
1029 nextDestinationY += destinationHeight + destinationSpacing;
1030 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1031 expect(
1032 fourthIconRenderBox.localToGlobal(Offset.zero),
1033 equals(
1034 Offset(
1035 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1036 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1037 ),
1038 ),
1039 );
1040 });
1041
1042 testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (
1043 WidgetTester tester,
1044 ) async {
1045 // Padding at the top of the rail.
1046 const double topPadding = 8.0;
1047 // Width of a destination.
1048 const double destinationWidth = 80.0;
1049 // Height of a destination indicator with icon.
1050 const double destinationHeight = 32.0;
1051 // Space between the indicator and label.
1052 const double destinationLabelSpacing = 4.0;
1053 // Height of the label.
1054 const double labelHeight = 16.0;
1055 // Height of a destination with both icon and label.
1056 const double destinationHeightWithLabel =
1057 destinationHeight + destinationLabelSpacing + labelHeight;
1058 // Space between destinations.
1059 const double destinationSpacing = 12.0;
1060
1061 await _pumpNavigationRail(
1062 tester,
1063 navigationRail: NavigationRail(
1064 selectedIndex: 0,
1065 destinations: _destinations(),
1066 labelType: NavigationRailLabelType.all,
1067 ),
1068 );
1069
1070 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1071 expect(renderBox.size.width, destinationWidth);
1072
1073 // The first destination topPadding below the rail top.
1074 double nextDestinationY = topPadding;
1075 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1076 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
1077 expect(
1078 firstIconRenderBox.localToGlobal(Offset.zero),
1079 equals(
1080 Offset(
1081 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1082 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1083 ),
1084 ),
1085 );
1086 expect(
1087 firstLabelRenderBox.localToGlobal(Offset.zero),
1088 equals(
1089 Offset(
1090 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
1091 nextDestinationY + destinationHeight + destinationLabelSpacing,
1092 ),
1093 ),
1094 );
1095
1096 // The second destination is below the first with some spacing.
1097 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1098 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1099 expect(
1100 secondIconRenderBox.localToGlobal(Offset.zero),
1101 equals(
1102 Offset(
1103 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1104 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1105 ),
1106 ),
1107 );
1108
1109 // The third destination is below the second with some spacing.
1110 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1111 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1112 expect(
1113 thirdIconRenderBox.localToGlobal(Offset.zero),
1114 equals(
1115 Offset(
1116 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1117 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1118 ),
1119 ),
1120 );
1121
1122 // The fourth destination is below the third with some spacing.
1123 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1124 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1125 expect(
1126 fourthIconRenderBox.localToGlobal(Offset.zero),
1127 equals(
1128 Offset(
1129 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1130 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1131 ),
1132 ),
1133 );
1134 });
1135
1136 testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (
1137 WidgetTester tester,
1138 ) async {
1139 // Padding at the top of the rail.
1140 const double topPadding = 8.0;
1141 // Width of a destination.
1142 const double destinationWidth = 125.5;
1143 // Height of a destination indicator with icon.
1144 const double destinationHeight = 32.0;
1145 // Space between the indicator and label.
1146 const double destinationLabelSpacing = 4.0;
1147 // Height of the label.
1148 const double labelHeight = 16.0 * 3.0;
1149 // Height of a destination with both icon and label.
1150 const double destinationHeightWithLabel =
1151 destinationHeight + destinationLabelSpacing + labelHeight;
1152 // Space between destinations.
1153 const double destinationSpacing = 12.0;
1154
1155 await _pumpNavigationRail(
1156 tester,
1157 textScaleFactor: 3.0,
1158 navigationRail: NavigationRail(
1159 selectedIndex: 0,
1160 destinations: _destinations(),
1161 labelType: NavigationRailLabelType.all,
1162 ),
1163 );
1164
1165 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1166 expect(renderBox.size.width, destinationWidth);
1167
1168 // The first destination topPadding below the rail top.
1169 double nextDestinationY = topPadding;
1170 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1171 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
1172 expect(
1173 firstIconRenderBox.localToGlobal(Offset.zero),
1174 equals(
1175 Offset(
1176 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1177 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1178 ),
1179 ),
1180 );
1181 expect(
1182 firstLabelRenderBox.localToGlobal(Offset.zero),
1183 equals(
1184 Offset(
1185 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
1186 nextDestinationY + destinationHeight + destinationLabelSpacing,
1187 ),
1188 ),
1189 );
1190
1191 // The second destination is below the first with some spacing.
1192 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1193 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1194 expect(
1195 secondIconRenderBox.localToGlobal(Offset.zero),
1196 equals(
1197 Offset(
1198 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1199 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1200 ),
1201 ),
1202 );
1203
1204 // The third destination is below the second with some spacing.
1205 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1206 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1207 expect(
1208 thirdIconRenderBox.localToGlobal(Offset.zero),
1209 equals(
1210 Offset(
1211 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1212 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1213 ),
1214 ),
1215 );
1216
1217 // The fourth destination is below the third with some spacing.
1218 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1219 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1220 expect(
1221 fourthIconRenderBox.localToGlobal(Offset.zero),
1222 equals(
1223 Offset(
1224 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1225 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1226 ),
1227 ),
1228 );
1229 });
1230
1231 testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (
1232 WidgetTester tester,
1233 ) async {
1234 // Padding at the top of the rail.
1235 const double topPadding = 8.0;
1236 // Width of a destination.
1237 const double destinationWidth = 80.0;
1238 // Height of a destination indicator with icon.
1239 const double destinationHeight = 32.0;
1240 // Space between the indicator and label.
1241 const double destinationLabelSpacing = 4.0;
1242 // Height of the label.
1243 const double labelHeight = 16.0 * 0.75;
1244 // Height of a destination with both icon and label.
1245 const double destinationHeightWithLabel =
1246 destinationHeight + destinationLabelSpacing + labelHeight;
1247 // Space between destinations.
1248 const double destinationSpacing = 12.0;
1249
1250 await _pumpNavigationRail(
1251 tester,
1252 textScaleFactor: 0.75,
1253 navigationRail: NavigationRail(
1254 selectedIndex: 0,
1255 destinations: _destinations(),
1256 labelType: NavigationRailLabelType.all,
1257 ),
1258 );
1259
1260 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1261 expect(renderBox.size.width, destinationWidth);
1262
1263 // The first destination topPadding below the rail top.
1264 double nextDestinationY = topPadding;
1265 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1266 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
1267 expect(
1268 firstIconRenderBox.localToGlobal(Offset.zero),
1269 equals(
1270 Offset(
1271 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1272 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1273 ),
1274 ),
1275 );
1276 expect(
1277 firstLabelRenderBox.localToGlobal(Offset.zero),
1278 equals(
1279 Offset(
1280 (destinationWidth - firstLabelRenderBox.size.width) / 2.0,
1281 nextDestinationY + destinationHeight + destinationLabelSpacing,
1282 ),
1283 ),
1284 );
1285
1286 // The second destination is below the first with some spacing.
1287 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1288 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1289 expect(
1290 secondIconRenderBox.localToGlobal(Offset.zero),
1291 equals(
1292 Offset(
1293 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1294 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1295 ),
1296 ),
1297 );
1298
1299 // The third destination is below the second with some spacing.
1300 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1301 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1302 expect(
1303 thirdIconRenderBox.localToGlobal(Offset.zero),
1304 equals(
1305 Offset(
1306 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1307 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1308 ),
1309 ),
1310 );
1311
1312 // The fourth destination is below the third with some spacing.
1313 nextDestinationY += destinationHeightWithLabel + destinationSpacing;
1314 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1315 expect(
1316 fourthIconRenderBox.localToGlobal(Offset.zero),
1317 equals(
1318 Offset(
1319 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1320 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1321 ),
1322 ),
1323 );
1324 });
1325
1326 testWidgets(
1327 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)',
1328 (WidgetTester tester) async {
1329 // Padding at the top of the rail.
1330 const double topPadding = 8.0;
1331 // Width of a destination.
1332 const double compactWidth = 56.0;
1333 // Height of a destination indicator with icon.
1334 const double destinationHeight = 32.0;
1335 // Space between destinations.
1336 const double destinationSpacing = 12.0;
1337
1338 await _pumpNavigationRail(
1339 tester,
1340 navigationRail: NavigationRail(
1341 selectedIndex: 0,
1342 minWidth: 56.0,
1343 destinations: _destinations(),
1344 ),
1345 );
1346
1347 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1348 expect(renderBox.size.width, 56.0);
1349
1350 // The first destination below the rail top by some padding.
1351 double nextDestinationY = topPadding + destinationSpacing / 2;
1352 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1353 expect(
1354 firstIconRenderBox.localToGlobal(Offset.zero),
1355 equals(
1356 Offset(
1357 (compactWidth - firstIconRenderBox.size.width) / 2.0,
1358 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1359 ),
1360 ),
1361 );
1362
1363 // The second destination is row below the first destination.
1364 nextDestinationY += destinationHeight + destinationSpacing;
1365 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1366 expect(
1367 secondIconRenderBox.localToGlobal(Offset.zero),
1368 equals(
1369 Offset(
1370 (compactWidth - secondIconRenderBox.size.width) / 2.0,
1371 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1372 ),
1373 ),
1374 );
1375
1376 // The third destination is a row below the second destination.
1377 nextDestinationY += destinationHeight + destinationSpacing;
1378 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1379 expect(
1380 thirdIconRenderBox.localToGlobal(Offset.zero),
1381 equals(
1382 Offset(
1383 (compactWidth - thirdIconRenderBox.size.width) / 2.0,
1384 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1385 ),
1386 ),
1387 );
1388
1389 // The fourth destination is a row below the third destination.
1390 nextDestinationY += destinationHeight + destinationSpacing;
1391 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1392 expect(
1393 fourthIconRenderBox.localToGlobal(Offset.zero),
1394 equals(
1395 Offset(
1396 (compactWidth - fourthIconRenderBox.size.width) / 2.0,
1397 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1398 ),
1399 ),
1400 );
1401 },
1402 );
1403
1404 testWidgets(
1405 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0',
1406 (WidgetTester tester) async {
1407 // Padding at the top of the rail.
1408 const double topPadding = 8.0;
1409 // Width of a destination.
1410 const double compactWidth = 56.0;
1411 // Height of a destination indicator with icon.
1412 const double destinationHeight = 32.0;
1413 // Space between destinations.
1414 const double destinationSpacing = 12.0;
1415
1416 await _pumpNavigationRail(
1417 tester,
1418 textScaleFactor: 3.0,
1419 navigationRail: NavigationRail(
1420 selectedIndex: 0,
1421 minWidth: 56.0,
1422 destinations: _destinations(),
1423 ),
1424 );
1425
1426 // Since the rail is icon only, its preferred width should not be affected
1427 // by textScaleFactor.
1428 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1429 expect(renderBox.size.width, compactWidth);
1430
1431 // The first destination below the rail top by some padding.
1432 double nextDestinationY = topPadding + destinationSpacing / 2;
1433 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1434 expect(
1435 firstIconRenderBox.localToGlobal(Offset.zero),
1436 equals(
1437 Offset(
1438 (compactWidth - firstIconRenderBox.size.width) / 2.0,
1439 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1440 ),
1441 ),
1442 );
1443
1444 // The second destination is row below the first destination.
1445 nextDestinationY += destinationHeight + destinationSpacing;
1446 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1447 expect(
1448 secondIconRenderBox.localToGlobal(Offset.zero),
1449 equals(
1450 Offset(
1451 (compactWidth - secondIconRenderBox.size.width) / 2.0,
1452 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1453 ),
1454 ),
1455 );
1456
1457 // The third destination is a row below the second destination.
1458 nextDestinationY += destinationHeight + destinationSpacing;
1459 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1460 expect(
1461 thirdIconRenderBox.localToGlobal(Offset.zero),
1462 equals(
1463 Offset(
1464 (compactWidth - thirdIconRenderBox.size.width) / 2.0,
1465 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1466 ),
1467 ),
1468 );
1469
1470 // The fourth destination is a row below the third destination.
1471 nextDestinationY += destinationHeight + destinationSpacing;
1472 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1473 expect(
1474 fourthIconRenderBox.localToGlobal(Offset.zero),
1475 equals(
1476 Offset(
1477 (compactWidth - fourthIconRenderBox.size.width) / 2.0,
1478 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1479 ),
1480 ),
1481 );
1482 },
1483 );
1484
1485 testWidgets(
1486 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75',
1487 (WidgetTester tester) async {
1488 // Padding at the top of the rail.
1489 const double topPadding = 8.0;
1490 // Width of a destination.
1491 const double compactWidth = 56.0;
1492 // Height of a destination indicator with icon.
1493 const double destinationHeight = 32.0;
1494 // Space between destinations.
1495 const double destinationSpacing = 12.0;
1496
1497 await _pumpNavigationRail(
1498 tester,
1499 textScaleFactor: 0.75,
1500 navigationRail: NavigationRail(
1501 selectedIndex: 0,
1502 minWidth: 56.0,
1503 destinations: _destinations(),
1504 ),
1505 );
1506
1507 // Since the rail is icon only, its preferred width should not be affected
1508 // by textScaleFactor.
1509 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1510 expect(renderBox.size.width, compactWidth);
1511
1512 // The first destination below the rail top by some padding.
1513 double nextDestinationY = topPadding + destinationSpacing / 2;
1514 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1515 expect(
1516 firstIconRenderBox.localToGlobal(Offset.zero),
1517 equals(
1518 Offset(
1519 (compactWidth - firstIconRenderBox.size.width) / 2.0,
1520 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1521 ),
1522 ),
1523 );
1524
1525 // The second destination is row below the first destination.
1526 nextDestinationY += destinationHeight + destinationSpacing;
1527 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1528 expect(
1529 secondIconRenderBox.localToGlobal(Offset.zero),
1530 equals(
1531 Offset(
1532 (compactWidth - secondIconRenderBox.size.width) / 2.0,
1533 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1534 ),
1535 ),
1536 );
1537
1538 // The third destination is a row below the second destination.
1539 nextDestinationY += destinationHeight + destinationSpacing;
1540 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1541 expect(
1542 thirdIconRenderBox.localToGlobal(Offset.zero),
1543 equals(
1544 Offset(
1545 (compactWidth - thirdIconRenderBox.size.width) / 2.0,
1546 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1547 ),
1548 ),
1549 );
1550
1551 // The fourth destination is a row below the third destination.
1552 nextDestinationY += destinationHeight + destinationSpacing;
1553 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1554 expect(
1555 fourthIconRenderBox.localToGlobal(Offset.zero),
1556 equals(
1557 Offset(
1558 (compactWidth - fourthIconRenderBox.size.width) / 2.0,
1559 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1560 ),
1561 ),
1562 );
1563 },
1564 );
1565
1566 testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (
1567 WidgetTester tester,
1568 ) async {
1569 // Padding at the top of the rail.
1570 const double topPadding = 8.0;
1571 // Width of a destination.
1572 const double destinationWidth = 80.0;
1573 // Height of a destination indicator with icon.
1574 const double destinationHeight = 32.0;
1575 // Space between destinations.
1576 const double destinationPadding = 12.0;
1577
1578 await _pumpNavigationRail(
1579 tester,
1580 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
1581 );
1582
1583 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1584 expect(renderBox.size.width, destinationWidth);
1585
1586 // The first destination below the rail top by some padding.
1587 double nextDestinationY = topPadding + destinationPadding / 2;
1588 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1589 expect(
1590 firstIconRenderBox.localToGlobal(Offset.zero),
1591 equals(
1592 Offset(
1593 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1594 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1595 ),
1596 ),
1597 );
1598
1599 // The second destination is one height below the first destination.
1600 nextDestinationY += destinationHeight + destinationPadding;
1601 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1602 expect(
1603 secondIconRenderBox.localToGlobal(Offset.zero),
1604 equals(
1605 Offset(
1606 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1607 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1608 ),
1609 ),
1610 );
1611
1612 // The third destination is one height below the second destination.
1613 nextDestinationY += destinationHeight + destinationPadding;
1614 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1615 expect(
1616 thirdIconRenderBox.localToGlobal(Offset.zero),
1617 equals(
1618 Offset(
1619 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1620 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1621 ),
1622 ),
1623 );
1624
1625 // The fourth destination is one height below the third destination.
1626 nextDestinationY += destinationHeight + destinationPadding;
1627 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1628 expect(
1629 fourthIconRenderBox.localToGlobal(Offset.zero),
1630 equals(
1631 Offset(
1632 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1633 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1634 ),
1635 ),
1636 );
1637 });
1638
1639 testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async {
1640 // Padding at the top of the rail.
1641 const double topPadding = 8.0;
1642 // Width of a destination.
1643 const double destinationWidth = 80.0;
1644 // Height of a destination indicator with icon.
1645 const double destinationHeight = 32.0;
1646 // Space between destinations.
1647 const double destinationPadding = 12.0;
1648
1649 await _pumpNavigationRail(
1650 tester,
1651 navigationRail: NavigationRail(
1652 selectedIndex: 0,
1653 groupAlignment: 0.0,
1654 destinations: _destinations(),
1655 ),
1656 );
1657
1658 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1659 expect(renderBox.size.width, destinationWidth);
1660
1661 // The first destination below the rail top by some padding with an offset for the alignment.
1662 double nextDestinationY = topPadding + destinationPadding / 2 + 208;
1663 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1664 expect(
1665 firstIconRenderBox.localToGlobal(Offset.zero),
1666 equals(
1667 Offset(
1668 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1669 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1670 ),
1671 ),
1672 );
1673
1674 // The second destination is one height below the first destination.
1675 nextDestinationY += destinationHeight + destinationPadding;
1676 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1677 expect(
1678 secondIconRenderBox.localToGlobal(Offset.zero),
1679 equals(
1680 Offset(
1681 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1682 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1683 ),
1684 ),
1685 );
1686
1687 // The third destination is one height below the second destination.
1688 nextDestinationY += destinationHeight + destinationPadding;
1689 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1690 expect(
1691 thirdIconRenderBox.localToGlobal(Offset.zero),
1692 equals(
1693 Offset(
1694 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1695 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1696 ),
1697 ),
1698 );
1699
1700 // The fourth destination is one height below the third destination.
1701 nextDestinationY += destinationHeight + destinationPadding;
1702 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1703 expect(
1704 fourthIconRenderBox.localToGlobal(Offset.zero),
1705 equals(
1706 Offset(
1707 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1708 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1709 ),
1710 ),
1711 );
1712 });
1713
1714 testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async {
1715 // Padding at the top of the rail.
1716 const double topPadding = 8.0;
1717 // Width of a destination.
1718 const double destinationWidth = 80.0;
1719 // Height of a destination indicator with icon.
1720 const double destinationHeight = 32.0;
1721 // Space between destinations.
1722 const double destinationPadding = 12.0;
1723
1724 await _pumpNavigationRail(
1725 tester,
1726 navigationRail: NavigationRail(
1727 selectedIndex: 0,
1728 groupAlignment: 1.0,
1729 destinations: _destinations(),
1730 ),
1731 );
1732
1733 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
1734 expect(renderBox.size.width, destinationWidth);
1735
1736 // The first destination below the rail top by some padding with an offset for the alignment.
1737 double nextDestinationY = topPadding + destinationPadding / 2 + 416;
1738 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1739 expect(
1740 firstIconRenderBox.localToGlobal(Offset.zero),
1741 equals(
1742 Offset(
1743 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1744 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1745 ),
1746 ),
1747 );
1748
1749 // The second destination is one height below the first destination.
1750 nextDestinationY += destinationHeight + destinationPadding;
1751 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1752 expect(
1753 secondIconRenderBox.localToGlobal(Offset.zero),
1754 equals(
1755 Offset(
1756 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1757 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1758 ),
1759 ),
1760 );
1761
1762 // The third destination is one height below the second destination.
1763 nextDestinationY += destinationHeight + destinationPadding;
1764 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1765 expect(
1766 thirdIconRenderBox.localToGlobal(Offset.zero),
1767 equals(
1768 Offset(
1769 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1770 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1771 ),
1772 ),
1773 );
1774
1775 // The fourth destination is one height below the third destination.
1776 nextDestinationY += destinationHeight + destinationPadding;
1777 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1778 expect(
1779 fourthIconRenderBox.localToGlobal(Offset.zero),
1780 equals(
1781 Offset(
1782 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1783 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1784 ),
1785 ),
1786 );
1787 });
1788
1789 testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async {
1790 await _pumpNavigationRail(
1791 tester,
1792 navigationRail: NavigationRail(
1793 selectedIndex: 0,
1794 leading: FloatingActionButton(onPressed: () {}),
1795 trailing: FloatingActionButton(onPressed: () {}),
1796 destinations: _destinations(),
1797 ),
1798 );
1799
1800 final RenderBox leading = tester.renderObject<RenderBox>(
1801 find.byType(FloatingActionButton).at(0),
1802 );
1803 final RenderBox trailing = tester.renderObject<RenderBox>(
1804 find.byType(FloatingActionButton).at(1),
1805 );
1806 expect(leading.localToGlobal(Offset.zero), Offset((80 - leading.size.width) / 2, 8.0));
1807 expect(trailing.localToGlobal(Offset.zero), Offset((80 - trailing.size.width) / 2, 248.0));
1808 });
1809
1810 testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (
1811 WidgetTester tester,
1812 ) async {
1813 // Padding at the top of the rail.
1814 const double topPadding = 8.0;
1815 // Width of a destination.
1816 const double destinationWidth = 80.0;
1817 // Height of a destination indicator with icon.
1818 const double destinationHeight = 32.0;
1819 // Space between destinations.
1820 const double destinationPadding = 12.0;
1821
1822 bool extended = false;
1823 late StateSetter stateSetter;
1824
1825 await tester.pumpWidget(
1826 MaterialApp(
1827 home: StatefulBuilder(
1828 builder: (BuildContext context, StateSetter setState) {
1829 stateSetter = setState;
1830 return Scaffold(
1831 body: Row(
1832 children: <Widget>[
1833 NavigationRail(
1834 selectedIndex: 0,
1835 destinations: _destinations(),
1836 extended: extended,
1837 ),
1838 const Expanded(child: Text('body')),
1839 ],
1840 ),
1841 );
1842 },
1843 ),
1844 ),
1845 );
1846
1847 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
1848
1849 expect(rail.size.width, destinationWidth);
1850
1851 stateSetter(() {
1852 extended = true;
1853 });
1854
1855 await tester.pump();
1856 await tester.pump(const Duration(milliseconds: 100));
1857 expect(rail.size.width, equals(168.0));
1858
1859 await tester.pumpAndSettle();
1860 expect(rail.size.width, equals(256.0));
1861
1862 // The first destination below the rail top by some padding.
1863 double nextDestinationY = topPadding + destinationPadding / 2;
1864 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
1865 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
1866 expect(
1867 firstIconRenderBox.localToGlobal(Offset.zero),
1868 equals(
1869 Offset(
1870 (destinationWidth - firstIconRenderBox.size.width) / 2.0,
1871 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
1872 ),
1873 ),
1874 );
1875 expect(
1876 firstLabelRenderBox.localToGlobal(Offset.zero),
1877 equals(
1878 Offset(
1879 destinationWidth,
1880 nextDestinationY + (destinationHeight - firstLabelRenderBox.size.height) / 2.0,
1881 ),
1882 ),
1883 );
1884
1885 // The second destination is one height below the first destination.
1886 nextDestinationY += destinationHeight + destinationPadding;
1887 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
1888 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
1889 expect(
1890 secondIconRenderBox.localToGlobal(Offset.zero),
1891 equals(
1892 Offset(
1893 (destinationWidth - secondIconRenderBox.size.width) / 2.0,
1894 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
1895 ),
1896 ),
1897 );
1898 expect(
1899 secondLabelRenderBox.localToGlobal(Offset.zero),
1900 equals(
1901 Offset(
1902 destinationWidth,
1903 nextDestinationY + (destinationHeight - secondLabelRenderBox.size.height) / 2.0,
1904 ),
1905 ),
1906 );
1907
1908 // The third destination is one height below the second destination.
1909 nextDestinationY += destinationHeight + destinationPadding;
1910 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
1911 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
1912 expect(
1913 thirdIconRenderBox.localToGlobal(Offset.zero),
1914 equals(
1915 Offset(
1916 (destinationWidth - thirdIconRenderBox.size.width) / 2.0,
1917 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
1918 ),
1919 ),
1920 );
1921 expect(
1922 thirdLabelRenderBox.localToGlobal(Offset.zero),
1923 equals(
1924 Offset(
1925 destinationWidth,
1926 nextDestinationY + (destinationHeight - thirdLabelRenderBox.size.height) / 2.0,
1927 ),
1928 ),
1929 );
1930
1931 // The fourth destination is one height below the third destination.
1932 nextDestinationY += destinationHeight + destinationPadding;
1933 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
1934 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
1935 expect(
1936 fourthIconRenderBox.localToGlobal(Offset.zero),
1937 equals(
1938 Offset(
1939 (destinationWidth - fourthIconRenderBox.size.width) / 2.0,
1940 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
1941 ),
1942 ),
1943 );
1944 expect(
1945 fourthLabelRenderBox.localToGlobal(Offset.zero),
1946 equals(
1947 Offset(
1948 destinationWidth,
1949 nextDestinationY + (destinationHeight - fourthLabelRenderBox.size.height) / 2.0,
1950 ),
1951 ),
1952 );
1953 });
1954
1955 testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (
1956 WidgetTester tester,
1957 ) async {
1958 // Padding at the top of the rail.
1959 const double topPadding = 8.0;
1960 // Width of a destination.
1961 const double destinationWidth = 80.0;
1962 // Height of a destination indicator with icon.
1963 const double destinationHeight = 32.0;
1964 // Space between destinations.
1965 const double destinationPadding = 12.0;
1966
1967 bool extended = false;
1968 late StateSetter stateSetter;
1969
1970 await tester.pumpWidget(
1971 MaterialApp(
1972 home: StatefulBuilder(
1973 builder: (BuildContext context, StateSetter setState) {
1974 stateSetter = setState;
1975 return Directionality(
1976 textDirection: TextDirection.rtl,
1977 child: Scaffold(
1978 body: Row(
1979 textDirection: TextDirection.rtl,
1980 children: <Widget>[
1981 NavigationRail(
1982 selectedIndex: 0,
1983 destinations: _destinations(),
1984 extended: extended,
1985 ),
1986 const Expanded(child: Text('body')),
1987 ],
1988 ),
1989 ),
1990 );
1991 },
1992 ),
1993 ),
1994 );
1995
1996 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
1997
1998 expect(rail.size.width, equals(destinationWidth));
1999 expect(rail.localToGlobal(Offset.zero), equals(const Offset(720.0, 0.0)));
2000
2001 stateSetter(() {
2002 extended = true;
2003 });
2004
2005 await tester.pump();
2006 await tester.pump(const Duration(milliseconds: 100));
2007 expect(rail.size.width, equals(168.0));
2008 expect(rail.localToGlobal(Offset.zero), equals(const Offset(632.0, 0.0)));
2009
2010 await tester.pumpAndSettle();
2011 expect(rail.size.width, equals(256.0));
2012 expect(rail.localToGlobal(Offset.zero), equals(const Offset(544.0, 0.0)));
2013
2014 // The first destination below the rail top by some padding.
2015 double nextDestinationY = topPadding + destinationPadding / 2;
2016 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
2017 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
2018 expect(
2019 firstIconRenderBox.localToGlobal(Offset.zero),
2020 equals(
2021 Offset(
2022 800.0 - (destinationWidth + firstIconRenderBox.size.width) / 2.0,
2023 nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0,
2024 ),
2025 ),
2026 );
2027 expect(
2028 firstLabelRenderBox.localToGlobal(Offset.zero),
2029 equals(
2030 Offset(
2031 800.0 - destinationWidth - firstLabelRenderBox.size.width,
2032 nextDestinationY + (destinationHeight - firstLabelRenderBox.size.height) / 2.0,
2033 ),
2034 ),
2035 );
2036
2037 // The second destination is one height below the first destination.
2038 nextDestinationY += destinationHeight + destinationPadding;
2039 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
2040 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
2041 expect(
2042 secondIconRenderBox.localToGlobal(Offset.zero),
2043 equals(
2044 Offset(
2045 800.0 - (destinationWidth + secondIconRenderBox.size.width) / 2.0,
2046 nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0,
2047 ),
2048 ),
2049 );
2050 expect(
2051 secondLabelRenderBox.localToGlobal(Offset.zero),
2052 equals(
2053 Offset(
2054 800.0 - destinationWidth - secondLabelRenderBox.size.width,
2055 nextDestinationY + (destinationHeight - secondLabelRenderBox.size.height) / 2.0,
2056 ),
2057 ),
2058 );
2059
2060 // The third destination is one height below the second destination.
2061 nextDestinationY += destinationHeight + destinationPadding;
2062 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
2063 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
2064 expect(
2065 thirdIconRenderBox.localToGlobal(Offset.zero),
2066 equals(
2067 Offset(
2068 800.0 - (destinationWidth + thirdIconRenderBox.size.width) / 2.0,
2069 nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0,
2070 ),
2071 ),
2072 );
2073 expect(
2074 thirdLabelRenderBox.localToGlobal(Offset.zero),
2075 equals(
2076 Offset(
2077 800.0 - destinationWidth - thirdLabelRenderBox.size.width,
2078 nextDestinationY + (destinationHeight - thirdLabelRenderBox.size.height) / 2.0,
2079 ),
2080 ),
2081 );
2082
2083 // The fourth destination is one height below the third destination.
2084 nextDestinationY += destinationHeight + destinationPadding;
2085 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
2086 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
2087 expect(
2088 fourthIconRenderBox.localToGlobal(Offset.zero),
2089 equals(
2090 Offset(
2091 800 - (destinationWidth + fourthIconRenderBox.size.width) / 2.0,
2092 nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0,
2093 ),
2094 ),
2095 );
2096 expect(
2097 fourthLabelRenderBox.localToGlobal(Offset.zero),
2098 equals(
2099 Offset(
2100 800.0 - destinationWidth - fourthLabelRenderBox.size.width,
2101 nextDestinationY + (destinationHeight - fourthLabelRenderBox.size.height) / 2.0,
2102 ),
2103 ),
2104 );
2105 });
2106
2107 testWidgets('Extended rail gets wider with longer labels are larger text scale', (
2108 WidgetTester tester,
2109 ) async {
2110 bool extended = false;
2111 late StateSetter stateSetter;
2112
2113 await tester.pumpWidget(
2114 MaterialApp(
2115 home: StatefulBuilder(
2116 builder: (BuildContext context, StateSetter setState) {
2117 stateSetter = setState;
2118 return Scaffold(
2119 body: Row(
2120 children: <Widget>[
2121 MediaQuery.withClampedTextScaling(
2122 minScaleFactor: 3.0,
2123 maxScaleFactor: 3.0,
2124 child: NavigationRail(
2125 selectedIndex: 0,
2126 destinations: const <NavigationRailDestination>[
2127 NavigationRailDestination(
2128 icon: Icon(Icons.favorite_border),
2129 selectedIcon: Icon(Icons.favorite),
2130 label: Text('Abc'),
2131 ),
2132 NavigationRailDestination(
2133 icon: Icon(Icons.bookmark_border),
2134 selectedIcon: Icon(Icons.bookmark),
2135 label: Text('Longer Label'),
2136 ),
2137 ],
2138 extended: extended,
2139 ),
2140 ),
2141 const Expanded(child: Text('body')),
2142 ],
2143 ),
2144 );
2145 },
2146 ),
2147 ),
2148 );
2149
2150 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
2151
2152 expect(rail.size.width, equals(80.0));
2153
2154 stateSetter(() {
2155 extended = true;
2156 });
2157
2158 await tester.pump();
2159 await tester.pump(const Duration(milliseconds: 100));
2160 expect(rail.size.width, equals(303.0));
2161
2162 await tester.pumpAndSettle();
2163 expect(rail.size.width, equals(526.0));
2164 });
2165
2166 testWidgets('Extended rail final width can be changed', (WidgetTester tester) async {
2167 bool extended = false;
2168 late StateSetter stateSetter;
2169
2170 await tester.pumpWidget(
2171 MaterialApp(
2172 home: StatefulBuilder(
2173 builder: (BuildContext context, StateSetter setState) {
2174 stateSetter = setState;
2175 return Scaffold(
2176 body: Row(
2177 children: <Widget>[
2178 NavigationRail(
2179 selectedIndex: 0,
2180 minExtendedWidth: 300,
2181 destinations: _destinations(),
2182 extended: extended,
2183 ),
2184 const Expanded(child: Text('body')),
2185 ],
2186 ),
2187 );
2188 },
2189 ),
2190 ),
2191 );
2192
2193 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
2194
2195 expect(rail.size.width, equals(80.0));
2196
2197 stateSetter(() {
2198 extended = true;
2199 });
2200
2201 await tester.pumpAndSettle();
2202 expect(rail.size.width, equals(300.0));
2203 });
2204
2205 /// Regression test for https://github.com/flutter/flutter/issues/65657
2206 testWidgets('Extended rail transition does not jump from the beginning', (
2207 WidgetTester tester,
2208 ) async {
2209 bool extended = false;
2210 late StateSetter stateSetter;
2211
2212 await tester.pumpWidget(
2213 MaterialApp(
2214 home: StatefulBuilder(
2215 builder: (BuildContext context, StateSetter setState) {
2216 stateSetter = setState;
2217 return Scaffold(
2218 body: Row(
2219 children: <Widget>[
2220 NavigationRail(
2221 selectedIndex: 0,
2222 destinations: const <NavigationRailDestination>[
2223 NavigationRailDestination(
2224 icon: Icon(Icons.favorite_border),
2225 selectedIcon: Icon(Icons.favorite),
2226 label: Text('Abc'),
2227 ),
2228 NavigationRailDestination(
2229 icon: Icon(Icons.bookmark_border),
2230 selectedIcon: Icon(Icons.bookmark),
2231 label: Text('Longer Label'),
2232 ),
2233 ],
2234 extended: extended,
2235 ),
2236 const Expanded(child: Text('body')),
2237 ],
2238 ),
2239 );
2240 },
2241 ),
2242 ),
2243 );
2244
2245 final Finder rail = find.byType(NavigationRail);
2246
2247 // Before starting the animation, the rail has a width of 80.
2248 expect(tester.getSize(rail).width, 80.0);
2249
2250 stateSetter(() {
2251 extended = true;
2252 });
2253
2254 await tester.pump();
2255 // Create very close to 0, but non-zero, animation value.
2256 await tester.pump(const Duration(milliseconds: 1));
2257 // Expect that it has started to extend.
2258 expect(tester.getSize(rail).width, greaterThan(80.0));
2259 // Expect that it has only extended by a small amount, or that the first
2260 // frame does not jump. This helps verify that it is a smooth animation.
2261 expect(tester.getSize(rail).width, closeTo(80.0, 1.0));
2262 });
2263
2264 testWidgets('Extended rail animation can be consumed', (WidgetTester tester) async {
2265 bool extended = false;
2266 late Animation<double> animation;
2267 late StateSetter stateSetter;
2268
2269 await tester.pumpWidget(
2270 MaterialApp(
2271 home: StatefulBuilder(
2272 builder: (BuildContext context, StateSetter setState) {
2273 stateSetter = setState;
2274 return Scaffold(
2275 body: Row(
2276 children: <Widget>[
2277 NavigationRail(
2278 selectedIndex: 0,
2279 leading: Builder(
2280 builder: (BuildContext context) {
2281 animation = NavigationRail.extendedAnimation(context);
2282 return FloatingActionButton(onPressed: () {});
2283 },
2284 ),
2285 destinations: _destinations(),
2286 extended: extended,
2287 ),
2288 const Expanded(child: Text('body')),
2289 ],
2290 ),
2291 );
2292 },
2293 ),
2294 ),
2295 );
2296
2297 expect(animation.isDismissed, isTrue);
2298
2299 stateSetter(() {
2300 extended = true;
2301 });
2302 await tester.pumpAndSettle();
2303
2304 expect(animation.isCompleted, isTrue);
2305 });
2306
2307 testWidgets('onDestinationSelected is called', (WidgetTester tester) async {
2308 late int selectedIndex;
2309
2310 await _pumpNavigationRail(
2311 tester,
2312 navigationRail: NavigationRail(
2313 selectedIndex: 0,
2314 destinations: _destinations(),
2315 onDestinationSelected: (int index) {
2316 selectedIndex = index;
2317 },
2318 labelType: NavigationRailLabelType.all,
2319 ),
2320 );
2321
2322 await tester.tap(find.text('Def'));
2323 expect(selectedIndex, 1);
2324
2325 await tester.tap(find.text('Ghi'));
2326 expect(selectedIndex, 2);
2327
2328 // Wait for any pending shader compilation.
2329 await tester.pumpAndSettle();
2330 });
2331
2332 testWidgets('onDestinationSelected is not called if null', (WidgetTester tester) async {
2333 const int selectedIndex = 0;
2334 await _pumpNavigationRail(
2335 tester,
2336 navigationRail: NavigationRail(
2337 selectedIndex: selectedIndex,
2338 destinations: _destinations(),
2339 labelType: NavigationRailLabelType.all,
2340 ),
2341 );
2342
2343 await tester.tap(find.text('Def'));
2344 expect(selectedIndex, 0);
2345
2346 // Wait for any pending shader compilation.
2347 await tester.pumpAndSettle();
2348 });
2349
2350 testWidgets('Changing destinations animate when [labelType]=selected', (
2351 WidgetTester tester,
2352 ) async {
2353 int selectedIndex = 0;
2354
2355 await tester.pumpWidget(
2356 MaterialApp(
2357 home: StatefulBuilder(
2358 builder: (BuildContext context, StateSetter setState) {
2359 return Scaffold(
2360 body: Row(
2361 children: <Widget>[
2362 NavigationRail(
2363 destinations: _destinations(),
2364 selectedIndex: selectedIndex,
2365 labelType: NavigationRailLabelType.selected,
2366 onDestinationSelected: (int index) {
2367 setState(() {
2368 selectedIndex = index;
2369 });
2370 },
2371 ),
2372 const Expanded(child: Text('body')),
2373 ],
2374 ),
2375 );
2376 },
2377 ),
2378 ),
2379 );
2380
2381 // Tap the second destination.
2382 await tester.tap(find.byIcon(Icons.bookmark_border));
2383 expect(selectedIndex, 1);
2384
2385 // The second destination animates in.
2386 expect(_labelOpacity(tester, 'Def'), equals(0.0));
2387 await tester.pump();
2388 await tester.pump(const Duration(milliseconds: 100));
2389 expect(_labelOpacity(tester, 'Def'), equals(0.5));
2390 await tester.pumpAndSettle();
2391 expect(_labelOpacity(tester, 'Def'), equals(1.0));
2392
2393 // Tap the third destination.
2394 await tester.tap(find.byIcon(Icons.star_border));
2395 expect(selectedIndex, 2);
2396
2397 // The second destination animates out quickly and the third destination
2398 // animates in.
2399 expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
2400 await tester.pump();
2401 await tester.pump(const Duration(milliseconds: 25));
2402 expect(_labelOpacity(tester, 'Def'), equals(0.5));
2403 expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
2404 await tester.pump();
2405 await tester.pump(const Duration(milliseconds: 25));
2406 expect(_labelOpacity(tester, 'Def'), equals(0.0));
2407 expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
2408 await tester.pump();
2409 await tester.pump(const Duration(milliseconds: 50));
2410 expect(_labelOpacity(tester, 'Ghi'), equals(0.5));
2411 await tester.pumpAndSettle();
2412 expect(_labelOpacity(tester, 'Ghi'), equals(1.0));
2413 });
2414
2415 testWidgets('Changing destinations animate for selectedIndex=null', (WidgetTester tester) async {
2416 int? selectedIndex = 0;
2417 late StateSetter stateSetter;
2418
2419 await tester.pumpWidget(
2420 MaterialApp(
2421 home: StatefulBuilder(
2422 builder: (BuildContext context, StateSetter setState) {
2423 stateSetter = setState;
2424 return Scaffold(
2425 body: Row(
2426 children: <Widget>[
2427 NavigationRail(
2428 destinations: _destinations(),
2429 selectedIndex: selectedIndex,
2430 labelType: NavigationRailLabelType.selected,
2431 ),
2432 const Expanded(child: Text('body')),
2433 ],
2434 ),
2435 );
2436 },
2437 ),
2438 ),
2439 );
2440
2441 // Unset the selected index.
2442 stateSetter(() {
2443 selectedIndex = null;
2444 });
2445
2446 // The first destination animates out.
2447 expect(_labelOpacity(tester, 'Abc'), equals(1.0));
2448 await tester.pump();
2449 await tester.pump(const Duration(milliseconds: 25));
2450 expect(_labelOpacity(tester, 'Abc'), equals(0.5));
2451 await tester.pumpAndSettle();
2452 expect(_labelOpacity(tester, 'Abc'), equals(0.0));
2453
2454 // Set the selected index to the first destination.
2455 stateSetter(() {
2456 selectedIndex = 0;
2457 });
2458
2459 // The first destination animates in.
2460 expect(_labelOpacity(tester, 'Abc'), equals(0.0));
2461 await tester.pump();
2462 await tester.pump(const Duration(milliseconds: 100));
2463 expect(_labelOpacity(tester, 'Abc'), equals(0.5));
2464 await tester.pumpAndSettle();
2465 expect(_labelOpacity(tester, 'Abc'), equals(1.0));
2466 });
2467
2468 testWidgets('Changing destinations animate when selectedIndex=null during transition', (
2469 WidgetTester tester,
2470 ) async {
2471 int? selectedIndex = 0;
2472 late StateSetter stateSetter;
2473
2474 await tester.pumpWidget(
2475 MaterialApp(
2476 home: StatefulBuilder(
2477 builder: (BuildContext context, StateSetter setState) {
2478 stateSetter = setState;
2479 return Scaffold(
2480 body: Row(
2481 children: <Widget>[
2482 NavigationRail(
2483 destinations: _destinations(),
2484 selectedIndex: selectedIndex,
2485 labelType: NavigationRailLabelType.selected,
2486 ),
2487 const Expanded(child: Text('body')),
2488 ],
2489 ),
2490 );
2491 },
2492 ),
2493 ),
2494 );
2495
2496 stateSetter(() {
2497 selectedIndex = 1;
2498 });
2499
2500 await tester.pump();
2501 await tester.pump(const Duration(milliseconds: 175));
2502
2503 // Interrupt while animating from index 0 to 1.
2504 stateSetter(() {
2505 selectedIndex = null;
2506 });
2507
2508 expect(_labelOpacity(tester, 'Abc'), equals(0));
2509 expect(_labelOpacity(tester, 'Def'), equals(1));
2510
2511 await tester.pump();
2512 // Create very close to 0, but non-zero, animation value.
2513 await tester.pump(const Duration(milliseconds: 1));
2514 // Ensure the opacity is animated back towards 0.
2515 expect(_labelOpacity(tester, 'Def'), lessThan(0.5));
2516 expect(_labelOpacity(tester, 'Def'), closeTo(0.5, 0.03));
2517
2518 await tester.pumpAndSettle();
2519 expect(_labelOpacity(tester, 'Abc'), equals(0.0));
2520 expect(_labelOpacity(tester, 'Def'), equals(0.0));
2521 });
2522
2523 testWidgets('Semantics - labelType=[none]', (WidgetTester tester) async {
2524 final SemanticsTester semantics = SemanticsTester(tester);
2525
2526 await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.none);
2527
2528 expect(
2529 semantics,
2530 hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true),
2531 );
2532
2533 semantics.dispose();
2534 });
2535
2536 testWidgets('Semantics - labelType=[selected]', (WidgetTester tester) async {
2537 final SemanticsTester semantics = SemanticsTester(tester);
2538
2539 await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.selected);
2540
2541 expect(
2542 semantics,
2543 hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true),
2544 );
2545
2546 semantics.dispose();
2547 });
2548
2549 testWidgets('Semantics - labelType=[all]', (WidgetTester tester) async {
2550 final SemanticsTester semantics = SemanticsTester(tester);
2551
2552 await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.all);
2553
2554 expect(
2555 semantics,
2556 hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true),
2557 );
2558
2559 semantics.dispose();
2560 });
2561
2562 testWidgets('Semantics - extended', (WidgetTester tester) async {
2563 final SemanticsTester semantics = SemanticsTester(tester);
2564
2565 await _pumpLocalizedTestRail(tester, extended: true);
2566
2567 expect(
2568 semantics,
2569 hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true),
2570 );
2571
2572 semantics.dispose();
2573 });
2574
2575 testWidgets('Semantics - scrollable', (WidgetTester tester) async {
2576 final SemanticsTester semantics = SemanticsTester(tester);
2577
2578 await _pumpLocalizedTestRail(tester, scrollable: true);
2579
2580 expect(
2581 semantics,
2582 hasSemantics(
2583 _expectedSemantics(scrollable: true),
2584 ignoreId: true,
2585 ignoreTransform: true,
2586 ignoreRect: true,
2587 ),
2588 );
2589
2590 semantics.dispose();
2591 });
2592
2593 testWidgets('NavigationRailDestination padding properly applied - NavigationRailLabelType.all', (
2594 WidgetTester tester,
2595 ) async {
2596 const EdgeInsets defaultPadding = EdgeInsets.symmetric(horizontal: 8.0);
2597 const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0);
2598 const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0);
2599
2600 await _pumpNavigationRail(
2601 tester,
2602 navigationRail: NavigationRail(
2603 labelType: NavigationRailLabelType.all,
2604 selectedIndex: 0,
2605 destinations: const <NavigationRailDestination>[
2606 NavigationRailDestination(
2607 icon: Icon(Icons.favorite_border),
2608 selectedIcon: Icon(Icons.favorite),
2609 label: Text('Abc'),
2610 ),
2611 NavigationRailDestination(
2612 icon: Icon(Icons.bookmark_border),
2613 selectedIcon: Icon(Icons.bookmark),
2614 label: Text('Def'),
2615 padding: secondItemPadding,
2616 ),
2617 NavigationRailDestination(
2618 icon: Icon(Icons.star_border),
2619 selectedIcon: Icon(Icons.star),
2620 label: Text('Ghi'),
2621 padding: thirdItemPadding,
2622 ),
2623 ],
2624 ),
2625 );
2626
2627 final Iterable<Widget> indicatorInkWells = tester.allWidgets.where(
2628 (Widget object) => object.runtimeType.toString() == '_IndicatorInkWell',
2629 );
2630 final Padding firstItem = tester.widget<Padding>(
2631 find.descendant(
2632 of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'),
2633 matching: find.widgetWithText(Padding, 'Abc'),
2634 ),
2635 );
2636 final Padding secondItem = tester.widget<Padding>(
2637 find.descendant(
2638 of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'),
2639 matching: find.widgetWithText(Padding, 'Def'),
2640 ),
2641 );
2642 final Padding thirdItem = tester.widget<Padding>(
2643 find.descendant(
2644 of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'),
2645 matching: find.widgetWithText(Padding, 'Ghi'),
2646 ),
2647 );
2648
2649 expect(firstItem.padding, defaultPadding);
2650 expect(secondItem.padding, secondItemPadding);
2651 expect(thirdItem.padding, thirdItemPadding);
2652 });
2653
2654 testWidgets(
2655 'NavigationRailDestination padding properly applied - NavigationRailLabelType.selected',
2656 (WidgetTester tester) async {
2657 const EdgeInsets defaultPadding = EdgeInsets.symmetric(horizontal: 8.0);
2658 const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0);
2659 const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0);
2660
2661 await _pumpNavigationRail(
2662 tester,
2663 navigationRail: NavigationRail(
2664 labelType: NavigationRailLabelType.selected,
2665 selectedIndex: 0,
2666 destinations: const <NavigationRailDestination>[
2667 NavigationRailDestination(
2668 icon: Icon(Icons.favorite_border),
2669 selectedIcon: Icon(Icons.favorite),
2670 label: Text('Abc'),
2671 ),
2672 NavigationRailDestination(
2673 icon: Icon(Icons.bookmark_border),
2674 selectedIcon: Icon(Icons.bookmark),
2675 label: Text('Def'),
2676 padding: secondItemPadding,
2677 ),
2678 NavigationRailDestination(
2679 icon: Icon(Icons.star_border),
2680 selectedIcon: Icon(Icons.star),
2681 label: Text('Ghi'),
2682 padding: thirdItemPadding,
2683 ),
2684 ],
2685 ),
2686 );
2687
2688 final Iterable<Widget> indicatorInkWells = tester.allWidgets.where(
2689 (Widget object) => object.runtimeType.toString() == '_IndicatorInkWell',
2690 );
2691 final Padding firstItem = tester.widget<Padding>(
2692 find.descendant(
2693 of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'),
2694 matching: find.widgetWithText(Padding, 'Abc'),
2695 ),
2696 );
2697 final Padding secondItem = tester.widget<Padding>(
2698 find.descendant(
2699 of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'),
2700 matching: find.widgetWithText(Padding, 'Def'),
2701 ),
2702 );
2703 final Padding thirdItem = tester.widget<Padding>(
2704 find.descendant(
2705 of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'),
2706 matching: find.widgetWithText(Padding, 'Ghi'),
2707 ),
2708 );
2709
2710 expect(firstItem.padding, defaultPadding);
2711 expect(secondItem.padding, secondItemPadding);
2712 expect(thirdItem.padding, thirdItemPadding);
2713 },
2714 );
2715
2716 testWidgets('NavigationRailDestination padding properly applied - NavigationRailLabelType.none', (
2717 WidgetTester tester,
2718 ) async {
2719 const EdgeInsets defaultPadding = EdgeInsets.zero;
2720 const EdgeInsets secondItemPadding = EdgeInsets.symmetric(vertical: 30.0);
2721 const EdgeInsets thirdItemPadding = EdgeInsets.symmetric(horizontal: 10.0);
2722
2723 await _pumpNavigationRail(
2724 tester,
2725 navigationRail: NavigationRail(
2726 labelType: NavigationRailLabelType.none,
2727 selectedIndex: 0,
2728 destinations: const <NavigationRailDestination>[
2729 NavigationRailDestination(
2730 icon: Icon(Icons.favorite_border),
2731 selectedIcon: Icon(Icons.favorite),
2732 label: Text('Abc'),
2733 ),
2734 NavigationRailDestination(
2735 icon: Icon(Icons.bookmark_border),
2736 selectedIcon: Icon(Icons.bookmark),
2737 label: Text('Def'),
2738 padding: secondItemPadding,
2739 ),
2740 NavigationRailDestination(
2741 icon: Icon(Icons.star_border),
2742 selectedIcon: Icon(Icons.star),
2743 label: Text('Ghi'),
2744 padding: thirdItemPadding,
2745 ),
2746 ],
2747 ),
2748 );
2749
2750 final Iterable<Widget> indicatorInkWells = tester.allWidgets.where(
2751 (Widget object) => object.runtimeType.toString() == '_IndicatorInkWell',
2752 );
2753 final Padding firstItem = tester.widget<Padding>(
2754 find.descendant(
2755 of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'),
2756 matching: find.widgetWithText(Padding, 'Abc'),
2757 ),
2758 );
2759 final Padding secondItem = tester.widget<Padding>(
2760 find.descendant(
2761 of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'),
2762 matching: find.widgetWithText(Padding, 'Def'),
2763 ),
2764 );
2765 final Padding thirdItem = tester.widget<Padding>(
2766 find.descendant(
2767 of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'),
2768 matching: find.widgetWithText(Padding, 'Ghi'),
2769 ),
2770 );
2771
2772 expect(firstItem.padding, defaultPadding);
2773 expect(secondItem.padding, secondItemPadding);
2774 expect(thirdItem.padding, thirdItemPadding);
2775 });
2776
2777 testWidgets(
2778 'NavigationRailDestination adds indicator by default when ThemeData.useMaterial3 is true',
2779 (WidgetTester tester) async {
2780 await _pumpNavigationRail(
2781 tester,
2782 navigationRail: NavigationRail(
2783 labelType: NavigationRailLabelType.selected,
2784 selectedIndex: 0,
2785 destinations: const <NavigationRailDestination>[
2786 NavigationRailDestination(
2787 icon: Icon(Icons.favorite_border),
2788 selectedIcon: Icon(Icons.favorite),
2789 label: Text('Abc'),
2790 ),
2791 NavigationRailDestination(
2792 icon: Icon(Icons.bookmark_border),
2793 selectedIcon: Icon(Icons.bookmark),
2794 label: Text('Def'),
2795 ),
2796 NavigationRailDestination(
2797 icon: Icon(Icons.star_border),
2798 selectedIcon: Icon(Icons.star),
2799 label: Text('Ghi'),
2800 ),
2801 ],
2802 ),
2803 );
2804
2805 expect(find.byType(NavigationIndicator), findsWidgets);
2806 },
2807 );
2808
2809 testWidgets('NavigationRailDestination adds indicator when useIndicator is true', (
2810 WidgetTester tester,
2811 ) async {
2812 await _pumpNavigationRail(
2813 tester,
2814 navigationRail: NavigationRail(
2815 useIndicator: true,
2816 labelType: NavigationRailLabelType.selected,
2817 selectedIndex: 0,
2818 destinations: const <NavigationRailDestination>[
2819 NavigationRailDestination(
2820 icon: Icon(Icons.favorite_border),
2821 selectedIcon: Icon(Icons.favorite),
2822 label: Text('Abc'),
2823 ),
2824 NavigationRailDestination(
2825 icon: Icon(Icons.bookmark_border),
2826 selectedIcon: Icon(Icons.bookmark),
2827 label: Text('Def'),
2828 ),
2829 NavigationRailDestination(
2830 icon: Icon(Icons.star_border),
2831 selectedIcon: Icon(Icons.star),
2832 label: Text('Ghi'),
2833 ),
2834 ],
2835 ),
2836 );
2837
2838 expect(find.byType(NavigationIndicator), findsWidgets);
2839 });
2840
2841 testWidgets('NavigationRailDestination does not add indicator when useIndicator is false', (
2842 WidgetTester tester,
2843 ) async {
2844 await _pumpNavigationRail(
2845 tester,
2846 navigationRail: NavigationRail(
2847 useIndicator: false,
2848 labelType: NavigationRailLabelType.selected,
2849 selectedIndex: 0,
2850 destinations: const <NavigationRailDestination>[
2851 NavigationRailDestination(
2852 icon: Icon(Icons.favorite_border),
2853 selectedIcon: Icon(Icons.favorite),
2854 label: Text('Abc'),
2855 ),
2856 NavigationRailDestination(
2857 icon: Icon(Icons.bookmark_border),
2858 selectedIcon: Icon(Icons.bookmark),
2859 label: Text('Def'),
2860 ),
2861 NavigationRailDestination(
2862 icon: Icon(Icons.star_border),
2863 selectedIcon: Icon(Icons.star),
2864 label: Text('Ghi'),
2865 ),
2866 ],
2867 ),
2868 );
2869
2870 expect(find.byType(NavigationIndicator), findsNothing);
2871 });
2872
2873 testWidgets('NavigationRailDestination adds an oval indicator when no labels are present', (
2874 WidgetTester tester,
2875 ) async {
2876 await _pumpNavigationRail(
2877 tester,
2878 navigationRail: NavigationRail(
2879 useIndicator: true,
2880 labelType: NavigationRailLabelType.none,
2881 selectedIndex: 0,
2882 destinations: const <NavigationRailDestination>[
2883 NavigationRailDestination(
2884 icon: Icon(Icons.favorite_border),
2885 selectedIcon: Icon(Icons.favorite),
2886 label: Text('Abc'),
2887 ),
2888 NavigationRailDestination(
2889 icon: Icon(Icons.bookmark_border),
2890 selectedIcon: Icon(Icons.bookmark),
2891 label: Text('Def'),
2892 ),
2893 NavigationRailDestination(
2894 icon: Icon(Icons.star_border),
2895 selectedIcon: Icon(Icons.star),
2896 label: Text('Ghi'),
2897 ),
2898 ],
2899 ),
2900 );
2901
2902 final NavigationIndicator indicator = tester.widget<NavigationIndicator>(
2903 find.byType(NavigationIndicator).first,
2904 );
2905
2906 expect(indicator.width, 56);
2907 expect(indicator.height, 32);
2908 });
2909
2910 testWidgets('NavigationRailDestination adds an oval indicator when selected labels are present', (
2911 WidgetTester tester,
2912 ) async {
2913 await _pumpNavigationRail(
2914 tester,
2915 navigationRail: NavigationRail(
2916 useIndicator: true,
2917 labelType: NavigationRailLabelType.selected,
2918 selectedIndex: 0,
2919 destinations: const <NavigationRailDestination>[
2920 NavigationRailDestination(
2921 icon: Icon(Icons.favorite_border),
2922 selectedIcon: Icon(Icons.favorite),
2923 label: Text('Abc'),
2924 ),
2925 NavigationRailDestination(
2926 icon: Icon(Icons.bookmark_border),
2927 selectedIcon: Icon(Icons.bookmark),
2928 label: Text('Def'),
2929 ),
2930 NavigationRailDestination(
2931 icon: Icon(Icons.star_border),
2932 selectedIcon: Icon(Icons.star),
2933 label: Text('Ghi'),
2934 ),
2935 ],
2936 ),
2937 );
2938
2939 final NavigationIndicator indicator = tester.widget<NavigationIndicator>(
2940 find.byType(NavigationIndicator).first,
2941 );
2942
2943 expect(indicator.width, 56);
2944 expect(indicator.height, 32);
2945 });
2946
2947 testWidgets('NavigationRailDestination adds an oval indicator when all labels are present', (
2948 WidgetTester tester,
2949 ) async {
2950 await _pumpNavigationRail(
2951 tester,
2952 navigationRail: NavigationRail(
2953 useIndicator: true,
2954 labelType: NavigationRailLabelType.all,
2955 selectedIndex: 0,
2956 destinations: const <NavigationRailDestination>[
2957 NavigationRailDestination(
2958 icon: Icon(Icons.favorite_border),
2959 selectedIcon: Icon(Icons.favorite),
2960 label: Text('Abc'),
2961 ),
2962 NavigationRailDestination(
2963 icon: Icon(Icons.bookmark_border),
2964 selectedIcon: Icon(Icons.bookmark),
2965 label: Text('Def'),
2966 ),
2967 NavigationRailDestination(
2968 icon: Icon(Icons.star_border),
2969 selectedIcon: Icon(Icons.star),
2970 label: Text('Ghi'),
2971 ),
2972 ],
2973 ),
2974 );
2975
2976 final NavigationIndicator indicator = tester.widget<NavigationIndicator>(
2977 find.byType(NavigationIndicator).first,
2978 );
2979
2980 expect(indicator.width, 56);
2981 expect(indicator.height, 32);
2982 });
2983
2984 testWidgets('NavigationRailDestination has center aligned indicator - [labelType]=none', (
2985 WidgetTester tester,
2986 ) async {
2987 // This is a regression test for
2988 // https://github.com/flutter/flutter/issues/97753
2989 await _pumpNavigationRail(
2990 tester,
2991 navigationRail: NavigationRail(
2992 labelType: NavigationRailLabelType.none,
2993 selectedIndex: 0,
2994 destinations: const <NavigationRailDestination>[
2995 NavigationRailDestination(
2996 icon: Stack(
2997 children: <Widget>[
2998 Icon(Icons.umbrella),
2999 Positioned(
3000 top: 0,
3001 right: 0,
3002 child: Text('Text', style: TextStyle(fontSize: 10, color: Colors.red)),
3003 ),
3004 ],
3005 ),
3006 label: Text('Abc'),
3007 ),
3008 NavigationRailDestination(icon: Icon(Icons.umbrella), label: Text('Def')),
3009 NavigationRailDestination(icon: Icon(Icons.bookmark_border), label: Text('Ghi')),
3010 ],
3011 ),
3012 );
3013 // Indicator with Stack widget
3014 final RenderBox firstIndicator = tester.renderObject(find.byType(Icon).first);
3015 expect(firstIndicator.localToGlobal(Offset.zero).dx, 28.0);
3016 // Indicator without Stack widget
3017 final RenderBox lastIndicator = tester.renderObject(find.byType(Icon).last);
3018 expect(lastIndicator.localToGlobal(Offset.zero).dx, 28.0);
3019 });
3020
3021 testWidgets('NavigationRail respects the notch/system navigation bar in landscape mode', (
3022 WidgetTester tester,
3023 ) async {
3024 const double safeAreaPadding = 40.0;
3025 NavigationRail navigationRail() {
3026 return NavigationRail(
3027 selectedIndex: 0,
3028 destinations: const <NavigationRailDestination>[
3029 NavigationRailDestination(
3030 icon: Icon(Icons.favorite_border),
3031 selectedIcon: Icon(Icons.favorite),
3032 label: Text('Abc'),
3033 ),
3034 NavigationRailDestination(
3035 icon: Icon(Icons.bookmark_border),
3036 selectedIcon: Icon(Icons.bookmark),
3037 label: Text('Def'),
3038 ),
3039 ],
3040 );
3041 }
3042
3043 await tester.pumpWidget(_buildWidget(navigationRail()));
3044 final double defaultWidth = tester.getSize(find.byType(NavigationRail)).width;
3045 expect(defaultWidth, 80);
3046
3047 await tester.pumpWidget(
3048 _buildWidget(
3049 MediaQuery(
3050 data: const MediaQueryData(padding: EdgeInsets.only(left: safeAreaPadding)),
3051 child: navigationRail(),
3052 ),
3053 ),
3054 );
3055 final double updatedWidth = tester.getSize(find.byType(NavigationRail)).width;
3056 expect(updatedWidth, defaultWidth + safeAreaPadding);
3057
3058 // test width when text direction is RTL.
3059 await tester.pumpWidget(
3060 _buildWidget(
3061 MediaQuery(
3062 data: const MediaQueryData(padding: EdgeInsets.only(right: safeAreaPadding)),
3063 child: navigationRail(),
3064 ),
3065 isRTL: true,
3066 ),
3067 );
3068 final double updatedWidthRTL = tester.getSize(find.byType(NavigationRail)).width;
3069 expect(updatedWidthRTL, defaultWidth + safeAreaPadding);
3070 });
3071
3072 testWidgets('NavigationRail indicator renders ripple', (WidgetTester tester) async {
3073 await _pumpNavigationRail(
3074 tester,
3075 navigationRail: NavigationRail(
3076 selectedIndex: 1,
3077 destinations: const <NavigationRailDestination>[
3078 NavigationRailDestination(
3079 icon: Icon(Icons.favorite_border),
3080 selectedIcon: Icon(Icons.favorite),
3081 label: Text('Abc'),
3082 ),
3083 NavigationRailDestination(
3084 icon: Icon(Icons.bookmark_border),
3085 selectedIcon: Icon(Icons.bookmark),
3086 label: Text('Def'),
3087 ),
3088 ],
3089 labelType: NavigationRailLabelType.all,
3090 ),
3091 );
3092
3093 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3094 await gesture.addPointer();
3095 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3096 await tester.pumpAndSettle();
3097
3098 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3099 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3100 );
3101 const Rect indicatorRect = Rect.fromLTRB(12.0, 0.0, 68.0, 32.0);
3102 const Rect includedRect = indicatorRect;
3103 final Rect excludedRect = includedRect.inflate(10);
3104
3105 expect(
3106 inkFeatures,
3107 paints
3108 ..clipPath(
3109 pathMatcher: isPathThat(
3110 includes: <Offset>[
3111 includedRect.centerLeft,
3112 includedRect.topCenter,
3113 includedRect.centerRight,
3114 includedRect.bottomCenter,
3115 ],
3116 excludes: <Offset>[
3117 excludedRect.centerLeft,
3118 excludedRect.topCenter,
3119 excludedRect.centerRight,
3120 excludedRect.bottomCenter,
3121 ],
3122 ),
3123 )
3124 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3125 ..rrect(
3126 rrect: RRect.fromLTRBR(12.0, 72.0, 68.0, 104.0, const Radius.circular(16)),
3127 color: const Color(0xffe8def8),
3128 ),
3129 );
3130 });
3131
3132 testWidgets('NavigationRail indicator renders ripple - extended', (WidgetTester tester) async {
3133 // This is a regression test for https://github.com/flutter/flutter/issues/117126
3134 await _pumpNavigationRail(
3135 tester,
3136 navigationRail: NavigationRail(
3137 selectedIndex: 1,
3138 extended: true,
3139 destinations: const <NavigationRailDestination>[
3140 NavigationRailDestination(
3141 icon: Icon(Icons.favorite_border),
3142 selectedIcon: Icon(Icons.favorite),
3143 label: Text('Abc'),
3144 ),
3145 NavigationRailDestination(
3146 icon: Icon(Icons.bookmark_border),
3147 selectedIcon: Icon(Icons.bookmark),
3148 label: Text('Def'),
3149 ),
3150 ],
3151 labelType: NavigationRailLabelType.none,
3152 ),
3153 );
3154
3155 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3156 await gesture.addPointer();
3157 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3158 await tester.pumpAndSettle();
3159
3160 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3161 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3162 );
3163 const Rect indicatorRect = Rect.fromLTRB(12.0, 6.0, 68.0, 38.0);
3164 const Rect includedRect = indicatorRect;
3165 final Rect excludedRect = includedRect.inflate(10);
3166
3167 expect(
3168 inkFeatures,
3169 paints
3170 ..clipPath(
3171 pathMatcher: isPathThat(
3172 includes: <Offset>[
3173 includedRect.centerLeft,
3174 includedRect.topCenter,
3175 includedRect.centerRight,
3176 includedRect.bottomCenter,
3177 ],
3178 excludes: <Offset>[
3179 excludedRect.centerLeft,
3180 excludedRect.topCenter,
3181 excludedRect.centerRight,
3182 excludedRect.bottomCenter,
3183 ],
3184 ),
3185 )
3186 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3187 ..rrect(
3188 rrect: RRect.fromLTRBR(12.0, 58.0, 68.0, 90.0, const Radius.circular(16)),
3189 color: const Color(0xffe8def8),
3190 ),
3191 );
3192 });
3193
3194 testWidgets('NavigationRail indicator renders properly when padding is applied', (
3195 WidgetTester tester,
3196 ) async {
3197 // This is a regression test for https://github.com/flutter/flutter/issues/117126
3198 await _pumpNavigationRail(
3199 tester,
3200 navigationRail: NavigationRail(
3201 selectedIndex: 1,
3202 extended: true,
3203 destinations: const <NavigationRailDestination>[
3204 NavigationRailDestination(
3205 padding: EdgeInsets.all(10),
3206 icon: Icon(Icons.favorite_border),
3207 selectedIcon: Icon(Icons.favorite),
3208 label: Text('Abc'),
3209 ),
3210 NavigationRailDestination(
3211 padding: EdgeInsets.all(18),
3212 icon: Icon(Icons.bookmark_border),
3213 selectedIcon: Icon(Icons.bookmark),
3214 label: Text('Def'),
3215 ),
3216 ],
3217 labelType: NavigationRailLabelType.none,
3218 ),
3219 );
3220
3221 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3222 await gesture.addPointer();
3223 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3224 await tester.pumpAndSettle();
3225
3226 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3227 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3228 );
3229 const Rect indicatorRect = Rect.fromLTRB(22.0, 16.0, 78.0, 48.0);
3230 const Rect includedRect = indicatorRect;
3231 final Rect excludedRect = includedRect.inflate(10);
3232
3233 expect(
3234 inkFeatures,
3235 paints
3236 ..clipPath(
3237 pathMatcher: isPathThat(
3238 includes: <Offset>[
3239 includedRect.centerLeft,
3240 includedRect.topCenter,
3241 includedRect.centerRight,
3242 includedRect.bottomCenter,
3243 ],
3244 excludes: <Offset>[
3245 excludedRect.centerLeft,
3246 excludedRect.topCenter,
3247 excludedRect.centerRight,
3248 excludedRect.bottomCenter,
3249 ],
3250 ),
3251 )
3252 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3253 ..rrect(
3254 rrect: RRect.fromLTRBR(30.0, 96.0, 86.0, 128.0, const Radius.circular(16)),
3255 color: const Color(0xffe8def8),
3256 ),
3257 );
3258 });
3259
3260 testWidgets('Indicator renders properly when NavigationRai.minWidth < default minWidth', (
3261 WidgetTester tester,
3262 ) async {
3263 // This is a regression test for https://github.com/flutter/flutter/issues/117126
3264 await _pumpNavigationRail(
3265 tester,
3266 navigationRail: NavigationRail(
3267 minWidth: 50,
3268 selectedIndex: 1,
3269 extended: true,
3270 destinations: const <NavigationRailDestination>[
3271 NavigationRailDestination(
3272 icon: Icon(Icons.favorite_border),
3273 selectedIcon: Icon(Icons.favorite),
3274 label: Text('Abc'),
3275 ),
3276 NavigationRailDestination(
3277 icon: Icon(Icons.bookmark_border),
3278 selectedIcon: Icon(Icons.bookmark),
3279 label: Text('Def'),
3280 ),
3281 ],
3282 labelType: NavigationRailLabelType.none,
3283 ),
3284 );
3285
3286 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3287 await gesture.addPointer();
3288 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3289 await tester.pumpAndSettle();
3290
3291 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3292 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3293 );
3294 const Rect indicatorRect = Rect.fromLTRB(-3.0, 6.0, 53.0, 38.0);
3295 const Rect includedRect = indicatorRect;
3296 final Rect excludedRect = includedRect.inflate(10);
3297
3298 expect(
3299 inkFeatures,
3300 paints
3301 ..clipPath(
3302 pathMatcher: isPathThat(
3303 includes: <Offset>[
3304 includedRect.centerLeft,
3305 includedRect.topCenter,
3306 includedRect.centerRight,
3307 includedRect.bottomCenter,
3308 ],
3309 excludes: <Offset>[
3310 excludedRect.centerLeft,
3311 excludedRect.topCenter,
3312 excludedRect.centerRight,
3313 excludedRect.bottomCenter,
3314 ],
3315 ),
3316 )
3317 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3318 ..rrect(
3319 rrect: RRect.fromLTRBR(0.0, 58.0, 50.0, 90.0, const Radius.circular(16)),
3320 color: const Color(0xffe8def8),
3321 ),
3322 );
3323 });
3324
3325 testWidgets('NavigationRail indicator renders properly with custom padding and minWidth', (
3326 WidgetTester tester,
3327 ) async {
3328 // This is a regression test for https://github.com/flutter/flutter/issues/117126
3329 await _pumpNavigationRail(
3330 tester,
3331 navigationRail: NavigationRail(
3332 minWidth: 300,
3333 selectedIndex: 1,
3334 extended: true,
3335 destinations: const <NavigationRailDestination>[
3336 NavigationRailDestination(
3337 padding: EdgeInsets.all(10),
3338 icon: Icon(Icons.favorite_border),
3339 selectedIcon: Icon(Icons.favorite),
3340 label: Text('Abc'),
3341 ),
3342 NavigationRailDestination(
3343 padding: EdgeInsets.all(18),
3344 icon: Icon(Icons.bookmark_border),
3345 selectedIcon: Icon(Icons.bookmark),
3346 label: Text('Def'),
3347 ),
3348 ],
3349 labelType: NavigationRailLabelType.none,
3350 ),
3351 );
3352
3353 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3354 await gesture.addPointer();
3355 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3356 await tester.pumpAndSettle();
3357
3358 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3359 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3360 );
3361 const Rect indicatorRect = Rect.fromLTRB(132.0, 16.0, 188.0, 48.0);
3362 const Rect includedRect = indicatorRect;
3363 final Rect excludedRect = includedRect.inflate(10);
3364
3365 expect(
3366 inkFeatures,
3367 paints
3368 ..clipPath(
3369 pathMatcher: isPathThat(
3370 includes: <Offset>[
3371 includedRect.centerLeft,
3372 includedRect.topCenter,
3373 includedRect.centerRight,
3374 includedRect.bottomCenter,
3375 ],
3376 excludes: <Offset>[
3377 excludedRect.centerLeft,
3378 excludedRect.topCenter,
3379 excludedRect.centerRight,
3380 excludedRect.bottomCenter,
3381 ],
3382 ),
3383 )
3384 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3385 ..rrect(
3386 rrect: RRect.fromLTRBR(140.0, 96.0, 196.0, 128.0, const Radius.circular(16)),
3387 color: const Color(0xffe8def8),
3388 ),
3389 );
3390 });
3391
3392 testWidgets('NavigationRail indicator renders properly with long labels', (
3393 WidgetTester tester,
3394 ) async {
3395 // This is a regression test for https://github.com/flutter/flutter/issues/128005.
3396 await _pumpNavigationRail(
3397 tester,
3398 navigationRail: NavigationRail(
3399 selectedIndex: 1,
3400 destinations: const <NavigationRailDestination>[
3401 NavigationRailDestination(
3402 icon: Icon(Icons.favorite_border),
3403 selectedIcon: Icon(Icons.favorite),
3404 label: Text('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
3405 ),
3406 NavigationRailDestination(
3407 icon: Icon(Icons.bookmark_border),
3408 selectedIcon: Icon(Icons.bookmark),
3409 label: Text('ABC'),
3410 ),
3411 ],
3412 labelType: NavigationRailLabelType.all,
3413 ),
3414 );
3415
3416 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3417 await gesture.addPointer();
3418 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3419 await tester.pumpAndSettle();
3420
3421 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3422 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3423 );
3424
3425 // Default values from M3 specification.
3426 const double indicatorHeight = 32.0;
3427 const double destinationWidth = 72.0;
3428 const double destinationHorizontalPadding = 8.0;
3429 const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0
3430 const double verticalSpacer = 8.0;
3431 const double verticalIconLabelSpacing = 4.0;
3432 const double verticalDestinationSpacing = 12.0;
3433
3434 // The navigation rail width is larger than default because of the first destination long label.
3435 final double railWidth = tester.getSize(find.byType(NavigationRail)).width;
3436
3437 // Expected indicator position.
3438 final double indicatorLeft = (railWidth - indicatorWidth) / 2;
3439 final double indicatorRight = (railWidth + indicatorWidth) / 2;
3440 final Rect indicatorRect = Rect.fromLTRB(indicatorLeft, 0.0, indicatorRight, indicatorHeight);
3441 final Rect includedRect = indicatorRect;
3442 final Rect excludedRect = includedRect.inflate(10);
3443
3444 // Compute the vertical position for the selected destination (the one with 'bookmark' icon).
3445 const double labelHeight = 16; // fontSize is 12 and height is 1.3.
3446 const double destinationHeight =
3447 indicatorHeight + verticalIconLabelSpacing + labelHeight + verticalDestinationSpacing;
3448 const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight;
3449 const double secondIndicatorVerticalOffset = secondDestinationVerticalOffset;
3450
3451 expect(
3452 inkFeatures,
3453 paints
3454 ..clipPath(
3455 pathMatcher: isPathThat(
3456 includes: <Offset>[
3457 includedRect.centerLeft,
3458 includedRect.topCenter,
3459 includedRect.centerRight,
3460 includedRect.bottomCenter,
3461 ],
3462 excludes: <Offset>[
3463 excludedRect.centerLeft,
3464 excludedRect.topCenter,
3465 excludedRect.centerRight,
3466 excludedRect.bottomCenter,
3467 ],
3468 ),
3469 )
3470 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3471 ..rrect(
3472 rrect: RRect.fromLTRBR(
3473 indicatorLeft,
3474 secondIndicatorVerticalOffset,
3475 indicatorRight,
3476 secondIndicatorVerticalOffset + indicatorHeight,
3477 const Radius.circular(16),
3478 ),
3479 color: const Color(0xffe8def8),
3480 ),
3481 );
3482 });
3483
3484 testWidgets('NavigationRail indicator renders properly with large icon', (
3485 WidgetTester tester,
3486 ) async {
3487 // This is a regression test for https://github.com/flutter/flutter/issues/133799.
3488 const double iconSize = 50;
3489 await _pumpNavigationRail(
3490 tester,
3491 navigationRailTheme: const NavigationRailThemeData(
3492 selectedIconTheme: IconThemeData(size: iconSize),
3493 unselectedIconTheme: IconThemeData(size: iconSize),
3494 ),
3495 navigationRail: NavigationRail(
3496 selectedIndex: 1,
3497 destinations: const <NavigationRailDestination>[
3498 NavigationRailDestination(
3499 icon: Icon(Icons.favorite_border),
3500 selectedIcon: Icon(Icons.favorite),
3501 label: Text('ABC'),
3502 ),
3503 NavigationRailDestination(
3504 icon: Icon(Icons.bookmark_border),
3505 selectedIcon: Icon(Icons.bookmark),
3506 label: Text('DEF'),
3507 ),
3508 ],
3509 labelType: NavigationRailLabelType.all,
3510 ),
3511 );
3512
3513 // Hover the first destination.
3514 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3515 await gesture.addPointer();
3516 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3517 await tester.pumpAndSettle();
3518
3519 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3520 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3521 );
3522
3523 // Default values from M3 specification.
3524 const double railMinWidth = 80.0;
3525 const double indicatorHeight = 32.0;
3526 const double destinationWidth = 72.0;
3527 const double destinationHorizontalPadding = 8.0;
3528 const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0
3529 const double verticalSpacer = 8.0;
3530 const double verticalIconLabelSpacing = 4.0;
3531 const double verticalDestinationSpacing = 12.0;
3532
3533 // The navigation rail width is the default one because labels are short.
3534 final double railWidth = tester.getSize(find.byType(NavigationRail)).width;
3535 expect(railWidth, railMinWidth);
3536
3537 // Expected indicator position.
3538 final double indicatorLeft = (railWidth - indicatorWidth) / 2;
3539 final double indicatorRight = (railWidth + indicatorWidth) / 2;
3540 const double indicatorTop = (iconSize - indicatorHeight) / 2;
3541 const double indicatorBottom = (iconSize + indicatorHeight) / 2;
3542 final Rect indicatorRect = Rect.fromLTRB(
3543 indicatorLeft,
3544 indicatorTop,
3545 indicatorRight,
3546 indicatorBottom,
3547 );
3548 final Rect includedRect = indicatorRect;
3549 final Rect excludedRect = includedRect.inflate(10);
3550
3551 // Compute the vertical position for the selected destination (the one with 'bookmark' icon).
3552 const double labelHeight = 16; // fontSize is 12 and height is 1.3.
3553 const double destinationHeight =
3554 iconSize + verticalIconLabelSpacing + labelHeight + verticalDestinationSpacing;
3555 const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight;
3556 const double indicatorOffset = (iconSize - indicatorHeight) / 2;
3557 const double secondIndicatorVerticalOffset = secondDestinationVerticalOffset + indicatorOffset;
3558
3559 expect(
3560 inkFeatures,
3561 paints
3562 ..clipPath(
3563 pathMatcher: isPathThat(
3564 includes: <Offset>[
3565 includedRect.centerLeft,
3566 includedRect.topCenter,
3567 includedRect.centerRight,
3568 includedRect.bottomCenter,
3569 ],
3570 excludes: <Offset>[
3571 excludedRect.centerLeft,
3572 excludedRect.topCenter,
3573 excludedRect.centerRight,
3574 excludedRect.bottomCenter,
3575 ],
3576 ),
3577 )
3578 // Hover highlight for the hovered destination (the one with 'favorite' icon).
3579 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3580 // Indicator for the selected destination (the one with 'bookmark' icon).
3581 ..rrect(
3582 rrect: RRect.fromLTRBR(
3583 indicatorLeft,
3584 secondIndicatorVerticalOffset,
3585 indicatorRight,
3586 secondIndicatorVerticalOffset + indicatorHeight,
3587 const Radius.circular(16),
3588 ),
3589 color: const Color(0xffe8def8),
3590 ),
3591 );
3592 });
3593
3594 testWidgets('NavigationRail indicator renders properly when text direction is rtl', (
3595 WidgetTester tester,
3596 ) async {
3597 // This is a regression test for https://github.com/flutter/flutter/issues/134361.
3598 await tester.pumpWidget(
3599 _buildWidget(
3600 NavigationRail(
3601 selectedIndex: 1,
3602 extended: true,
3603 destinations: const <NavigationRailDestination>[
3604 NavigationRailDestination(
3605 icon: Icon(Icons.favorite_border),
3606 selectedIcon: Icon(Icons.favorite),
3607 label: Text('ABC'),
3608 ),
3609 NavigationRailDestination(
3610 icon: Icon(Icons.bookmark_border),
3611 selectedIcon: Icon(Icons.bookmark),
3612 label: Text('DEF'),
3613 ),
3614 ],
3615 ),
3616 isRTL: true,
3617 ),
3618 );
3619
3620 // Hover the first destination.
3621 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3622 await gesture.addPointer();
3623 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3624 await tester.pumpAndSettle();
3625
3626 final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3627 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3628 );
3629
3630 // Default values from M3 specification.
3631 const double railMinExtendedWidth = 256.0;
3632 const double indicatorHeight = 32.0;
3633 const double destinationWidth = 72.0;
3634 const double destinationHorizontalPadding = 8.0;
3635 const double indicatorWidth = destinationWidth - 2 * destinationHorizontalPadding; // 56.0
3636 const double verticalSpacer = 8.0;
3637 const double verticalDestinationSpacingM3 = 12.0;
3638
3639 // The navigation rail width is the default one because labels are short.
3640 final double railWidth = tester.getSize(find.byType(NavigationRail)).width;
3641 expect(railWidth, railMinExtendedWidth);
3642
3643 // Expected indicator position.
3644 final double indicatorLeft = railWidth - (destinationWidth - destinationHorizontalPadding / 2);
3645 final double indicatorRight = indicatorLeft + indicatorWidth;
3646 final Rect indicatorRect = Rect.fromLTRB(
3647 indicatorLeft,
3648 verticalDestinationSpacingM3 / 2,
3649 indicatorRight,
3650 verticalDestinationSpacingM3 / 2 + indicatorHeight,
3651 );
3652 final Rect includedRect = indicatorRect;
3653 final Rect excludedRect = includedRect.inflate(10);
3654
3655 // Compute the vertical position for the selected destination (the one with 'bookmark' icon).
3656 const double destinationHeight = indicatorHeight + verticalDestinationSpacingM3;
3657 const double secondDestinationVerticalOffset = verticalSpacer + destinationHeight;
3658 const double secondIndicatorVerticalOffset =
3659 secondDestinationVerticalOffset + verticalDestinationSpacingM3 / 2;
3660 const double secondDestinationHorizontalOffset = 800 - railMinExtendedWidth; // RTL.
3661
3662 expect(
3663 inkFeatures,
3664 paints
3665 ..clipPath(
3666 pathMatcher: isPathThat(
3667 includes: <Offset>[
3668 includedRect.centerLeft,
3669 includedRect.topCenter,
3670 includedRect.centerRight,
3671 includedRect.bottomCenter,
3672 ],
3673 excludes: <Offset>[
3674 excludedRect.centerLeft,
3675 excludedRect.topCenter,
3676 excludedRect.centerRight,
3677 excludedRect.bottomCenter,
3678 ],
3679 ),
3680 )
3681 // Hover highlight for the hovered destination (the one with 'favorite' icon).
3682 ..rect(rect: indicatorRect, color: const Color(0x0a6750a4))
3683 // Indicator for the selected destination (the one with 'bookmark' icon).
3684 ..rrect(
3685 rrect: RRect.fromLTRBR(
3686 secondDestinationHorizontalOffset + indicatorLeft,
3687 secondIndicatorVerticalOffset,
3688 secondDestinationHorizontalOffset + indicatorRight,
3689 secondIndicatorVerticalOffset + indicatorHeight,
3690 const Radius.circular(16),
3691 ),
3692 color: const Color(0xffe8def8),
3693 ),
3694 );
3695 });
3696
3697 testWidgets('NavigationRail indicator scale transform', (WidgetTester tester) async {
3698 int selectedIndex = 0;
3699 Future<void> buildWidget() async {
3700 await _pumpNavigationRail(
3701 tester,
3702 navigationRail: NavigationRail(
3703 selectedIndex: selectedIndex,
3704 destinations: const <NavigationRailDestination>[
3705 NavigationRailDestination(
3706 icon: Icon(Icons.favorite_border),
3707 selectedIcon: Icon(Icons.favorite),
3708 label: Text('Abc'),
3709 ),
3710 NavigationRailDestination(
3711 icon: Icon(Icons.bookmark_border),
3712 selectedIcon: Icon(Icons.bookmark),
3713 label: Text('Def'),
3714 ),
3715 ],
3716 labelType: NavigationRailLabelType.all,
3717 ),
3718 );
3719 }
3720
3721 await buildWidget();
3722 await tester.pumpAndSettle();
3723 final Finder transformFinder = find
3724 .descendant(of: find.byType(NavigationIndicator), matching: find.byType(Transform))
3725 .last;
3726 Matrix4 transform = tester.widget<Transform>(transformFinder).transform;
3727 expect(transform.getColumn(0)[0], 0.0);
3728
3729 selectedIndex = 1;
3730 await buildWidget();
3731 await tester.pump(const Duration(milliseconds: 100));
3732 transform = tester.widget<Transform>(transformFinder).transform;
3733 expect(transform.getColumn(0)[0], closeTo(0.9705023956298828, precisionErrorTolerance));
3734
3735 await tester.pump(const Duration(milliseconds: 100));
3736 transform = tester.widget<Transform>(transformFinder).transform;
3737 expect(transform.getColumn(0)[0], 1.0);
3738 });
3739
3740 testWidgets('Navigation destination updates indicator color and shape', (
3741 WidgetTester tester,
3742 ) async {
3743 final ThemeData theme = ThemeData();
3744 const Color color = Color(0xff0000ff);
3745 const ShapeBorder shape = RoundedRectangleBorder();
3746
3747 Widget buildNavigationRail({Color? indicatorColor, ShapeBorder? indicatorShape}) {
3748 return MaterialApp(
3749 theme: theme,
3750 home: Builder(
3751 builder: (BuildContext context) {
3752 return Scaffold(
3753 body: Row(
3754 children: <Widget>[
3755 NavigationRail(
3756 useIndicator: true,
3757 indicatorColor: indicatorColor,
3758 indicatorShape: indicatorShape,
3759 selectedIndex: 0,
3760 destinations: _destinations(),
3761 ),
3762 const Expanded(child: Text('body')),
3763 ],
3764 ),
3765 );
3766 },
3767 ),
3768 );
3769 }
3770
3771 await tester.pumpWidget(buildNavigationRail());
3772
3773 // Test default indicator color and shape.
3774 expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
3775 expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
3776
3777 await tester.pumpWidget(buildNavigationRail(indicatorColor: color, indicatorShape: shape));
3778
3779 // Test custom indicator color and shape.
3780 expect(_getIndicatorDecoration(tester)?.color, color);
3781 expect(_getIndicatorDecoration(tester)?.shape, shape);
3782 });
3783
3784 testWidgets("Destination's respect their disabled state", (WidgetTester tester) async {
3785 late int selectedIndex;
3786 await _pumpNavigationRail(
3787 tester,
3788 navigationRail: NavigationRail(
3789 selectedIndex: 0,
3790 destinations: const <NavigationRailDestination>[
3791 NavigationRailDestination(
3792 icon: Icon(Icons.favorite_border),
3793 selectedIcon: Icon(Icons.favorite),
3794 label: Text('Abc'),
3795 ),
3796 NavigationRailDestination(
3797 icon: Icon(Icons.star_border),
3798 selectedIcon: Icon(Icons.star),
3799 label: Text('Bcd'),
3800 ),
3801 NavigationRailDestination(
3802 icon: Icon(Icons.bookmark_border),
3803 selectedIcon: Icon(Icons.bookmark),
3804 label: Text('Cde'),
3805 disabled: true,
3806 ),
3807 ],
3808 onDestinationSelected: (int index) {
3809 selectedIndex = index;
3810 },
3811 labelType: NavigationRailLabelType.all,
3812 ),
3813 );
3814
3815 await tester.tap(find.text('Abc'));
3816 expect(selectedIndex, 0);
3817
3818 await tester.tap(find.text('Bcd'));
3819 expect(selectedIndex, 1);
3820
3821 await tester.tap(find.text('Cde'));
3822 expect(selectedIndex, 1);
3823
3824 // Wait for any pending shader compilation.
3825 await tester.pumpAndSettle();
3826 });
3827
3828 testWidgets("Destination's label with the right opacity while disabled", (
3829 WidgetTester tester,
3830 ) async {
3831 await _pumpNavigationRail(
3832 tester,
3833 navigationRail: NavigationRail(
3834 selectedIndex: 0,
3835 destinations: const <NavigationRailDestination>[
3836 NavigationRailDestination(
3837 icon: Icon(Icons.favorite_border),
3838 selectedIcon: Icon(Icons.favorite),
3839 label: Text('Abc'),
3840 ),
3841 NavigationRailDestination(
3842 icon: Icon(Icons.bookmark_border),
3843 selectedIcon: Icon(Icons.bookmark),
3844 label: Text('Bcd'),
3845 disabled: true,
3846 ),
3847 ],
3848 onDestinationSelected: (int index) {},
3849 labelType: NavigationRailLabelType.all,
3850 ),
3851 );
3852
3853 await tester.pumpAndSettle();
3854
3855 double? defaultTextStyleOpacity(String text) {
3856 return tester
3857 .widget<DefaultTextStyle>(
3858 find.ancestor(of: find.text(text), matching: find.byType(DefaultTextStyle)).first,
3859 )
3860 .style
3861 .color
3862 ?.opacity;
3863 }
3864
3865 final double? abcLabelOpacity = defaultTextStyleOpacity('Abc');
3866 final double? bcdLabelOpacity = defaultTextStyleOpacity('Bcd');
3867
3868 expect(abcLabelOpacity, 1.0);
3869 expect(bcdLabelOpacity, closeTo(0.38, 0.01));
3870 });
3871
3872 testWidgets('NavigationRail indicator inkwell can be transparent', (WidgetTester tester) async {
3873 // This is a regression test for https://github.com/flutter/flutter/issues/135866.
3874 final ThemeData theme = ThemeData(
3875 colorScheme: const ColorScheme.light().copyWith(primary: Colors.transparent),
3876 // Material 3 defaults to InkSparkle which is not testable using paints.
3877 splashFactory: InkSplash.splashFactory,
3878 );
3879 await tester.pumpWidget(
3880 MaterialApp(
3881 theme: theme,
3882 home: Builder(
3883 builder: (BuildContext context) {
3884 return Scaffold(
3885 body: Row(
3886 children: <Widget>[
3887 NavigationRail(
3888 selectedIndex: 1,
3889 destinations: const <NavigationRailDestination>[
3890 NavigationRailDestination(
3891 icon: Icon(Icons.favorite_border),
3892 selectedIcon: Icon(Icons.favorite),
3893 label: Text('ABC'),
3894 ),
3895 NavigationRailDestination(
3896 icon: Icon(Icons.bookmark_border),
3897 selectedIcon: Icon(Icons.bookmark),
3898 label: Text('DEF'),
3899 ),
3900 ],
3901 labelType: NavigationRailLabelType.all,
3902 ),
3903 const Expanded(child: Text('body')),
3904 ],
3905 ),
3906 );
3907 },
3908 ),
3909 ),
3910 );
3911
3912 final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3913 await gesture.addPointer();
3914 await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3915 await tester.pumpAndSettle();
3916 RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
3917 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3918 );
3919
3920 expect(inkFeatures, paints..rect(color: Colors.transparent));
3921
3922 await gesture.down(tester.getCenter(find.byIcon(Icons.favorite_border)));
3923 await tester.pump(); // Start the splash and highlight animations.
3924 await tester.pump(
3925 const Duration(milliseconds: 800),
3926 ); // Wait for splash and highlight to be well under way.
3927
3928 inkFeatures = tester.allRenderObjects.firstWhere(
3929 (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
3930 );
3931 expect(inkFeatures, paints..circle(color: Colors.transparent));
3932 });
3933
3934 testWidgets('Navigation rail can have expanded widgets inside', (WidgetTester tester) async {
3935 await _pumpNavigationRail(
3936 tester,
3937 navigationRail: NavigationRail(
3938 selectedIndex: 0,
3939 destinations: const <NavigationRailDestination>[
3940 NavigationRailDestination(icon: Icon(Icons.favorite_border), label: Text('Abc')),
3941 NavigationRailDestination(icon: Icon(Icons.bookmark_border), label: Text('Bcd')),
3942 ],
3943 trailing: const Expanded(child: Icon(Icons.search)),
3944 ),
3945 );
3946
3947 await tester.pumpAndSettle();
3948
3949 expect(tester.takeException(), isNull);
3950 });
3951
3952 testWidgets('NavigationRail labels shall not overflow if longer texts provided - extended', (
3953 WidgetTester tester,
3954 ) async {
3955 // Regression test for https://github.com/flutter/flutter/issues/110901.
3956 // The navigation rail has a narrow width constraint. The text should wrap.
3957 const String normalLabel = 'Abc';
3958 const String longLabel = 'Very long bookmark text for navigation destination';
3959 await tester.pumpWidget(
3960 MaterialApp(
3961 home: Builder(
3962 builder: (BuildContext context) {
3963 return Scaffold(
3964 body: Row(
3965 children: <Widget>[
3966 SizedBox(
3967 width: 140.0,
3968 child: NavigationRail(
3969 selectedIndex: 1,
3970 extended: true,
3971 destinations: const <NavigationRailDestination>[
3972 NavigationRailDestination(
3973 icon: Icon(Icons.favorite_border),
3974 selectedIcon: Icon(Icons.favorite),
3975 label: Text(normalLabel),
3976 ),
3977 NavigationRailDestination(
3978 icon: Icon(Icons.bookmark_border),
3979 selectedIcon: Icon(Icons.bookmark),
3980 label: Text(longLabel),
3981 ),
3982 ],
3983 ),
3984 ),
3985 const Expanded(child: Text('body')),
3986 ],
3987 ),
3988 );
3989 },
3990 ),
3991 ),
3992 );
3993
3994 expect(find.byType(NavigationRail), findsOneWidget);
3995 expect(find.text(normalLabel), findsOneWidget);
3996 expect(find.text(longLabel), findsOneWidget);
3997
3998 // If the widget manages to layout without throwing an overflow exception,
3999 // the test passes.
4000 expect(tester.takeException(), isNull);
4001 });
4002
4003 testWidgets('NavigationRail can scroll in low height', (WidgetTester tester) async {
4004 await tester.pumpWidget(
4005 MaterialApp(
4006 home: Builder(
4007 builder: (BuildContext context) {
4008 return MediaQuery(
4009 // Set Screen height with 300
4010 data: MediaQuery.of(context).copyWith(size: const Size(800, 300)),
4011 child: Scaffold(
4012 body: Row(
4013 children: <Widget>[
4014 // Set NavigationRail height with 100
4015 SizedBox(
4016 height: 100,
4017 child: NavigationRail(
4018 selectedIndex: 0,
4019 scrollable: true,
4020 destinations: const <NavigationRailDestination>[
4021 NavigationRailDestination(
4022 icon: Icon(Icons.favorite_border),
4023 selectedIcon: Icon(Icons.favorite),
4024 label: Text('Abc'),
4025 ),
4026 NavigationRailDestination(
4027 icon: Icon(Icons.bookmark_border),
4028 selectedIcon: Icon(Icons.bookmark),
4029 label: Text('Def'),
4030 ),
4031 NavigationRailDestination(
4032 icon: Icon(Icons.star_border),
4033 selectedIcon: Icon(Icons.star),
4034 label: Text('Ghi'),
4035 ),
4036 ],
4037 ),
4038 ),
4039 const Expanded(child: Text('body')),
4040 ],
4041 ),
4042 ),
4043 );
4044 },
4045 ),
4046 ),
4047 );
4048
4049 final ScrollableState scrollable = tester.state(find.byType(Scrollable));
4050 scrollable.position.jumpTo(500.0);
4051 expect(scrollable.position.pixels, equals(500.0));
4052 });
4053
4054 testWidgets(
4055 'NavigationRail leading widget is at top and trailing widget is at last destination (defaults)',
4056 (WidgetTester tester) async {
4057 const Key leadingKey = Key('leading');
4058 const Key trailingKey = Key('trailing');
4059 tester.view.physicalSize = const Size(800, 600);
4060 tester.view.devicePixelRatio = 1.0;
4061 await tester.pumpWidget(
4062 MaterialApp(
4063 home: Builder(
4064 builder: (BuildContext context) {
4065 return Scaffold(
4066 body: Row(
4067 children: <Widget>[
4068 SizedBox(
4069 width: 140.0,
4070 child: NavigationRail(
4071 selectedIndex: 0,
4072 extended: true,
4073 groupAlignment: 0.0,
4074 leading: const SizedBox(key: leadingKey, height: 50),
4075 trailing: const SizedBox(key: trailingKey, height: 50),
4076 destinations: const <NavigationRailDestination>[
4077 NavigationRailDestination(icon: Icon(Icons.favorite), label: Text('Abc')),
4078 NavigationRailDestination(icon: Icon(Icons.bookmark), label: Text('Def')),
4079 NavigationRailDestination(icon: Icon(Icons.star), label: Text('Ghi')),
4080 ],
4081 ),
4082 ),
4083 const Expanded(child: Text('body')),
4084 ],
4085 ),
4086 );
4087 },
4088 ),
4089 ),
4090 );
4091
4092 final Rect leadingRect = tester.getRect(find.byKey(leadingKey));
4093 final Rect trailingRect = tester.getRect(find.byKey(trailingKey));
4094 final Rect firstDestRect = tester.getRect(
4095 find.ancestor(of: find.byIcon(Icons.favorite), matching: find.byType(Semantics)).first,
4096 );
4097 final Rect lastDestRect = tester.getRect(
4098 find.ancestor(of: find.byIcon(Icons.star), matching: find.byType(Semantics)).first,
4099 );
4100
4101 expect(leadingRect.top, 8);
4102 expect(firstDestRect.top - leadingRect.bottom, greaterThan(100));
4103 expect(trailingRect.top - lastDestRect.bottom, 0);
4104 expect(trailingRect.bottom, lessThan(500));
4105 },
4106 );
4107
4108 testWidgets('NavigationRail leadingAtTop set to false and trailingAtBottom to true', (
4109 WidgetTester tester,
4110 ) async {
4111 const Key leadingKey = Key('leading');
4112 const Key trailingKey = Key('trailing');
4113 tester.view.physicalSize = const Size(800, 600);
4114 tester.view.devicePixelRatio = 1.0;
4115 await tester.pumpWidget(
4116 MaterialApp(
4117 home: Builder(
4118 builder: (BuildContext context) {
4119 return Scaffold(
4120 body: Row(
4121 children: <Widget>[
4122 SizedBox(
4123 width: 140.0,
4124 child: NavigationRail(
4125 selectedIndex: 0,
4126 extended: true,
4127 groupAlignment: 0.0,
4128 leadingAtTop: false,
4129 trailingAtBottom: true,
4130 leading: const SizedBox(key: leadingKey, height: 50),
4131 trailing: const SizedBox(key: trailingKey, height: 50),
4132 destinations: const <NavigationRailDestination>[
4133 NavigationRailDestination(icon: Icon(Icons.favorite), label: Text('Abc')),
4134 NavigationRailDestination(icon: Icon(Icons.bookmark), label: Text('Def')),
4135 NavigationRailDestination(icon: Icon(Icons.star), label: Text('Ghi')),
4136 ],
4137 ),
4138 ),
4139 const Expanded(child: Text('body')),
4140 ],
4141 ),
4142 );
4143 },
4144 ),
4145 ),
4146 );
4147
4148 final Rect leadingRect = tester.getRect(find.byKey(leadingKey));
4149 final Rect trailingRect = tester.getRect(find.byKey(trailingKey));
4150 final Rect firstDestRect = tester.getRect(
4151 find.ancestor(of: find.byIcon(Icons.favorite), matching: find.byType(Semantics)).first,
4152 );
4153 final Rect lastDestRect = tester.getRect(
4154 find.ancestor(of: find.byIcon(Icons.star), matching: find.byType(Semantics)).first,
4155 );
4156
4157 expect(leadingRect.top, greaterThan(100));
4158 expect(firstDestRect.top - leadingRect.bottom, 8);
4159 expect(trailingRect.top - lastDestRect.bottom, greaterThan(100));
4160 expect(trailingRect.bottom, 600);
4161 });
4162
4163 group('Material 2', () {
4164 // These tests are only relevant for Material 2. Once Material 2
4165 // support is deprecated and the APIs are removed, these tests
4166 // can be deleted.
4167
4168 testWidgets('Renders at the correct default width - [labelType]=none (default)', (
4169 WidgetTester tester,
4170 ) async {
4171 await _pumpNavigationRail(
4172 tester,
4173 useMaterial3: false,
4174 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
4175 );
4176
4177 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4178 expect(renderBox.size.width, 72.0);
4179 });
4180
4181 testWidgets('Renders at the correct default width - [labelType]=selected', (
4182 WidgetTester tester,
4183 ) async {
4184 await _pumpNavigationRail(
4185 tester,
4186 useMaterial3: false,
4187 navigationRail: NavigationRail(
4188 selectedIndex: 0,
4189 labelType: NavigationRailLabelType.selected,
4190 destinations: _destinations(),
4191 ),
4192 );
4193
4194 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4195 expect(renderBox.size.width, 72.0);
4196 });
4197
4198 testWidgets('Renders at the correct default width - [labelType]=all', (
4199 WidgetTester tester,
4200 ) async {
4201 await _pumpNavigationRail(
4202 tester,
4203 useMaterial3: false,
4204 navigationRail: NavigationRail(
4205 selectedIndex: 0,
4206 labelType: NavigationRailLabelType.all,
4207 destinations: _destinations(),
4208 ),
4209 );
4210
4211 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4212 expect(renderBox.size.width, 72.0);
4213 });
4214
4215 testWidgets('Leading and trailing spacing is correct with 0~2 destinations', (
4216 WidgetTester tester,
4217 ) async {
4218 late StateSetter stateSetter;
4219 List<NavigationRailDestination> destinations = const <NavigationRailDestination>[];
4220 Widget? leadingWidget;
4221 Widget? trailingWidget;
4222
4223 const Key leadingWidgetKey = Key('leadingWidget');
4224 const Key trailingWidgetKey = Key('trailingWidget');
4225
4226 await tester.pumpWidget(
4227 MaterialApp(
4228 theme: ThemeData(useMaterial3: false),
4229 home: StatefulBuilder(
4230 builder: (BuildContext context, StateSetter setState) {
4231 stateSetter = setState;
4232 return Scaffold(
4233 body: Row(
4234 children: <Widget>[
4235 NavigationRail(
4236 destinations: destinations,
4237 selectedIndex: null,
4238 leading: leadingWidget,
4239 trailing: trailingWidget,
4240 ),
4241 const Expanded(child: Text('body')),
4242 ],
4243 ),
4244 );
4245 },
4246 ),
4247 ),
4248 );
4249
4250 // empty destinations and leading widget
4251 stateSetter(() {
4252 destinations = const <NavigationRailDestination>[];
4253 leadingWidget = FloatingActionButton(key: leadingWidgetKey, onPressed: () {});
4254 trailingWidget = null;
4255 });
4256 await tester.pumpAndSettle();
4257 expect(
4258 tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey)).localToGlobal(Offset.zero),
4259 const Offset(0, 8.0),
4260 );
4261
4262 // one destination and leading widget
4263 stateSetter(() {
4264 destinations = const <NavigationRailDestination>[
4265 NavigationRailDestination(
4266 icon: Icon(Icons.favorite_border),
4267 selectedIcon: Icon(Icons.favorite),
4268 label: Text('Abc'),
4269 ),
4270 ];
4271 });
4272 await tester.pumpAndSettle();
4273 expect(
4274 _iconRenderBox(tester, Icons.favorite_border).localToGlobal(Offset.zero),
4275 const Offset(24.0, 96.0),
4276 );
4277 expect(_labelRenderBox(tester, 'Abc').localToGlobal(Offset.zero), const Offset(0.0, 72.0));
4278 expect(
4279 tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey)).localToGlobal(Offset.zero),
4280 const Offset(8.0, 8.0),
4281 );
4282
4283 // two destinations and leading widget
4284 stateSetter(() {
4285 destinations = const <NavigationRailDestination>[
4286 NavigationRailDestination(
4287 icon: Icon(Icons.favorite_border),
4288 selectedIcon: Icon(Icons.favorite),
4289 label: Text('Abc'),
4290 ),
4291 NavigationRailDestination(
4292 icon: Icon(Icons.bookmark_border),
4293 selectedIcon: Icon(Icons.bookmark),
4294 label: Text('Longer Label'),
4295 ),
4296 ];
4297 });
4298 await tester.pumpAndSettle();
4299 expect(
4300 _iconRenderBox(tester, Icons.favorite_border).localToGlobal(Offset.zero),
4301 const Offset(24.0, 96.0),
4302 );
4303 expect(_labelRenderBox(tester, 'Abc').localToGlobal(Offset.zero), const Offset(0.0, 72.0));
4304 expect(
4305 _iconRenderBox(tester, Icons.bookmark_border).localToGlobal(Offset.zero),
4306 const Offset(24.0, 168.0),
4307 );
4308 expect(
4309 _labelRenderBox(tester, 'Longer Label').localToGlobal(Offset.zero),
4310 const Offset(0.0, 144.0),
4311 );
4312 expect(
4313 tester.renderObject<RenderBox>(find.byKey(leadingWidgetKey)).localToGlobal(Offset.zero),
4314 const Offset(8.0, 8.0),
4315 );
4316
4317 // empty destinations and trailing widget
4318 stateSetter(() {
4319 destinations = const <NavigationRailDestination>[];
4320 leadingWidget = null;
4321 trailingWidget = FloatingActionButton(key: trailingWidgetKey, onPressed: () {});
4322 });
4323 await tester.pumpAndSettle();
4324 expect(
4325 tester.renderObject<RenderBox>(find.byKey(trailingWidgetKey)).localToGlobal(Offset.zero),
4326 const Offset(0, 8.0),
4327 );
4328
4329 // one destination and trailing widget
4330 stateSetter(() {
4331 destinations = const <NavigationRailDestination>[
4332 NavigationRailDestination(
4333 icon: Icon(Icons.favorite_border),
4334 selectedIcon: Icon(Icons.favorite),
4335 label: Text('Abc'),
4336 ),
4337 ];
4338 });
4339 await tester.pumpAndSettle();
4340 expect(
4341 _iconRenderBox(tester, Icons.favorite_border).localToGlobal(Offset.zero),
4342 const Offset(24.0, 32.0),
4343 );
4344 expect(_labelRenderBox(tester, 'Abc').localToGlobal(Offset.zero), const Offset(0.0, 8.0));
4345 expect(
4346 tester.renderObject<RenderBox>(find.byKey(trailingWidgetKey)).localToGlobal(Offset.zero),
4347 const Offset(8.0, 80.0),
4348 );
4349
4350 // two destinations and trailing widget
4351 stateSetter(() {
4352 destinations = const <NavigationRailDestination>[
4353 NavigationRailDestination(
4354 icon: Icon(Icons.favorite_border),
4355 selectedIcon: Icon(Icons.favorite),
4356 label: Text('Abc'),
4357 ),
4358 NavigationRailDestination(
4359 icon: Icon(Icons.bookmark_border),
4360 selectedIcon: Icon(Icons.bookmark),
4361 label: Text('Longer Label'),
4362 ),
4363 ];
4364 });
4365 await tester.pumpAndSettle();
4366 expect(
4367 _iconRenderBox(tester, Icons.favorite_border).localToGlobal(Offset.zero),
4368 const Offset(24.0, 32.0),
4369 );
4370 expect(_labelRenderBox(tester, 'Abc').localToGlobal(Offset.zero), const Offset(0.0, 8.0));
4371 expect(
4372 _iconRenderBox(tester, Icons.bookmark_border).localToGlobal(Offset.zero),
4373 const Offset(24.0, 104.0),
4374 );
4375 expect(
4376 _labelRenderBox(tester, 'Longer Label').localToGlobal(Offset.zero),
4377 const Offset(0.0, 80.0),
4378 );
4379 expect(
4380 tester.renderObject<RenderBox>(find.byKey(trailingWidgetKey)).localToGlobal(Offset.zero),
4381 const Offset(8.0, 152.0),
4382 );
4383 });
4384
4385 testWidgets('Change destinations and selectedIndex', (WidgetTester tester) async {
4386 late StateSetter stateSetter;
4387 int? selectedIndex;
4388 List<NavigationRailDestination> destinations = const <NavigationRailDestination>[];
4389
4390 await tester.pumpWidget(
4391 MaterialApp(
4392 theme: ThemeData(useMaterial3: false),
4393 home: StatefulBuilder(
4394 builder: (BuildContext context, StateSetter setState) {
4395 stateSetter = setState;
4396 return Scaffold(
4397 body: Row(
4398 children: <Widget>[
4399 NavigationRail(selectedIndex: selectedIndex, destinations: destinations),
4400 const Expanded(child: Text('body')),
4401 ],
4402 ),
4403 );
4404 },
4405 ),
4406 ),
4407 );
4408
4409 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 0);
4410 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, isNull);
4411
4412 stateSetter(() {
4413 destinations = const <NavigationRailDestination>[
4414 NavigationRailDestination(
4415 icon: Icon(Icons.favorite_border),
4416 selectedIcon: Icon(Icons.favorite),
4417 label: Text('Abc'),
4418 ),
4419 NavigationRailDestination(
4420 icon: Icon(Icons.bookmark_border),
4421 selectedIcon: Icon(Icons.bookmark),
4422 label: Text('Longer Label'),
4423 ),
4424 ];
4425 });
4426
4427 await tester.pumpAndSettle();
4428
4429 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 2);
4430 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, isNull);
4431
4432 stateSetter(() {
4433 selectedIndex = 0;
4434 });
4435
4436 await tester.pumpAndSettle();
4437
4438 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 2);
4439 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, 0);
4440
4441 stateSetter(() {
4442 destinations = const <NavigationRailDestination>[
4443 NavigationRailDestination(
4444 icon: Icon(Icons.favorite_border),
4445 selectedIcon: Icon(Icons.favorite),
4446 label: Text('Abc'),
4447 ),
4448 ];
4449 });
4450
4451 await tester.pumpAndSettle();
4452
4453 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).destinations.length, 1);
4454 expect(tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex, 0);
4455 });
4456
4457 testWidgets(
4458 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)',
4459 (WidgetTester tester) async {
4460 await _pumpNavigationRail(
4461 tester,
4462 useMaterial3: false,
4463 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
4464 );
4465
4466 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4467 expect(renderBox.size.width, 72.0);
4468
4469 // The first destination is 8 from the top because of the default vertical
4470 // padding at the to of the rail.
4471 double nextDestinationY = 8.0;
4472 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4473 expect(
4474 firstIconRenderBox.localToGlobal(Offset.zero),
4475 equals(
4476 Offset(
4477 (72.0 - firstIconRenderBox.size.width) / 2.0,
4478 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
4479 ),
4480 ),
4481 );
4482
4483 // The second destination is 72 below the first destination.
4484 nextDestinationY += 72.0;
4485 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4486 expect(
4487 secondIconRenderBox.localToGlobal(Offset.zero),
4488 equals(
4489 Offset(
4490 (72.0 - secondIconRenderBox.size.width) / 2.0,
4491 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
4492 ),
4493 ),
4494 );
4495
4496 // The third destination is 72 below the second destination.
4497 nextDestinationY += 72.0;
4498 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4499 expect(
4500 thirdIconRenderBox.localToGlobal(Offset.zero),
4501 equals(
4502 Offset(
4503 (72.0 - thirdIconRenderBox.size.width) / 2.0,
4504 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
4505 ),
4506 ),
4507 );
4508
4509 // The fourth destination is 72 below the third destination.
4510 nextDestinationY += 72.0;
4511 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4512 expect(
4513 fourthIconRenderBox.localToGlobal(Offset.zero),
4514 equals(
4515 Offset(
4516 (72.0 - fourthIconRenderBox.size.width) / 2.0,
4517 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
4518 ),
4519 ),
4520 );
4521 },
4522 );
4523
4524 testWidgets(
4525 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0',
4526 (WidgetTester tester) async {
4527 // Since the rail is icon only, its destinations should not be affected by
4528 // textScaleFactor.
4529 await _pumpNavigationRail(
4530 tester,
4531 useMaterial3: false,
4532 textScaleFactor: 3.0,
4533 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
4534 );
4535
4536 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4537 expect(renderBox.size.width, 72.0);
4538
4539 // The first destination is 8 from the top because of the default vertical
4540 // padding at the to of the rail.
4541 double nextDestinationY = 8.0;
4542 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4543 expect(
4544 firstIconRenderBox.localToGlobal(Offset.zero),
4545 equals(
4546 Offset(
4547 (72.0 - firstIconRenderBox.size.width) / 2.0,
4548 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
4549 ),
4550 ),
4551 );
4552
4553 // The second destination is 72 below the first destination.
4554 nextDestinationY += 72.0;
4555 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4556 expect(
4557 secondIconRenderBox.localToGlobal(Offset.zero),
4558 equals(
4559 Offset(
4560 (72.0 - secondIconRenderBox.size.width) / 2.0,
4561 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
4562 ),
4563 ),
4564 );
4565
4566 // The third destination is 72 below the second destination.
4567 nextDestinationY += 72.0;
4568 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4569 expect(
4570 thirdIconRenderBox.localToGlobal(Offset.zero),
4571 equals(
4572 Offset(
4573 (72.0 - thirdIconRenderBox.size.width) / 2.0,
4574 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
4575 ),
4576 ),
4577 );
4578
4579 // The fourth destination is 72 below the third destination.
4580 nextDestinationY += 72.0;
4581 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4582 expect(
4583 fourthIconRenderBox.localToGlobal(Offset.zero),
4584 equals(
4585 Offset(
4586 (72.0 - fourthIconRenderBox.size.width) / 2.0,
4587 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
4588 ),
4589 ),
4590 );
4591 },
4592 );
4593
4594 testWidgets(
4595 'Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75',
4596 (WidgetTester tester) async {
4597 // Since the rail is icon only, its destinations should not be affected by
4598 // textScaleFactor.
4599 await _pumpNavigationRail(
4600 tester,
4601 useMaterial3: false,
4602 textScaleFactor: 0.75,
4603 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
4604 );
4605
4606 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4607 expect(renderBox.size.width, 72.0);
4608
4609 // The first destination is 8 from the top because of the default vertical
4610 // padding at the to of the rail.
4611 double nextDestinationY = 8.0;
4612 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4613 expect(
4614 firstIconRenderBox.localToGlobal(Offset.zero),
4615 equals(
4616 Offset(
4617 (72.0 - firstIconRenderBox.size.width) / 2.0,
4618 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
4619 ),
4620 ),
4621 );
4622
4623 // The second destination is 72 below the first destination.
4624 nextDestinationY += 72.0;
4625 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4626 expect(
4627 secondIconRenderBox.localToGlobal(Offset.zero),
4628 equals(
4629 Offset(
4630 (72.0 - secondIconRenderBox.size.width) / 2.0,
4631 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
4632 ),
4633 ),
4634 );
4635
4636 // The third destination is 72 below the second destination.
4637 nextDestinationY += 72.0;
4638 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4639 expect(
4640 thirdIconRenderBox.localToGlobal(Offset.zero),
4641 equals(
4642 Offset(
4643 (72.0 - thirdIconRenderBox.size.width) / 2.0,
4644 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
4645 ),
4646 ),
4647 );
4648
4649 // The fourth destination is 72 below the third destination.
4650 nextDestinationY += 72.0;
4651 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4652 expect(
4653 fourthIconRenderBox.localToGlobal(Offset.zero),
4654 equals(
4655 Offset(
4656 (72.0 - fourthIconRenderBox.size.width) / 2.0,
4657 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
4658 ),
4659 ),
4660 );
4661 },
4662 );
4663
4664 testWidgets(
4665 'Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)',
4666 (WidgetTester tester) async {
4667 await _pumpNavigationRail(
4668 tester,
4669 useMaterial3: false,
4670 navigationRail: NavigationRail(
4671 selectedIndex: 0,
4672 destinations: _destinations(),
4673 labelType: NavigationRailLabelType.selected,
4674 ),
4675 );
4676
4677 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4678 expect(renderBox.size.width, 72.0);
4679
4680 // The first destination is 8 from the top because of the default vertical
4681 // padding at the to of the rail.
4682 double nextDestinationY = 8.0;
4683 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4684 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
4685 expect(
4686 firstIconRenderBox.localToGlobal(Offset.zero),
4687 equals(
4688 Offset(
4689 (72.0 - firstIconRenderBox.size.width) / 2.0,
4690 nextDestinationY +
4691 (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
4692 ),
4693 ),
4694 );
4695 expect(
4696 firstLabelRenderBox.localToGlobal(Offset.zero),
4697 equals(
4698 Offset(
4699 (72.0 - firstLabelRenderBox.size.width) / 2.0,
4700 nextDestinationY +
4701 (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
4702 ),
4703 ),
4704 );
4705
4706 // The second destination is 72 below the first destination.
4707 nextDestinationY += 72.0;
4708 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4709 expect(
4710 secondIconRenderBox.localToGlobal(Offset.zero),
4711 equals(
4712 Offset(
4713 (72.0 - secondIconRenderBox.size.width) / 2.0,
4714 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
4715 ),
4716 ),
4717 );
4718
4719 // The third destination is 72 below the second destination.
4720 nextDestinationY += 72.0;
4721 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4722 expect(
4723 thirdIconRenderBox.localToGlobal(Offset.zero),
4724 equals(
4725 Offset(
4726 (72.0 - thirdIconRenderBox.size.width) / 2.0,
4727 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
4728 ),
4729 ),
4730 );
4731
4732 // The fourth destination is 72 below the third destination.
4733 nextDestinationY += 72.0;
4734 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4735 expect(
4736 fourthIconRenderBox.localToGlobal(Offset.zero),
4737 equals(
4738 Offset(
4739 (72.0 - fourthIconRenderBox.size.width) / 2.0,
4740 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
4741 ),
4742 ),
4743 );
4744 },
4745 );
4746
4747 testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (
4748 WidgetTester tester,
4749 ) async {
4750 await _pumpNavigationRail(
4751 tester,
4752 useMaterial3: false,
4753 textScaleFactor: 3.0,
4754 navigationRail: NavigationRail(
4755 selectedIndex: 0,
4756 destinations: _destinations(),
4757 labelType: NavigationRailLabelType.selected,
4758 ),
4759 );
4760
4761 // The rail and destinations sizes grow to fit the larger text labels.
4762 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4763 expect(renderBox.size.width, 142.0);
4764
4765 // The first destination is 8 from the top because of the default vertical
4766 // padding at the to of the rail.
4767 double nextDestinationY = 8.0;
4768 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4769 expect(
4770 firstIconRenderBox.localToGlobal(Offset.zero),
4771 equals(Offset((142.0 - firstIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
4772 );
4773
4774 // The first label sits right below the first icon.
4775 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
4776 expect(
4777 firstLabelRenderBox.localToGlobal(Offset.zero),
4778 equals(
4779 Offset(
4780 (142.0 - firstLabelRenderBox.size.width) / 2.0,
4781 nextDestinationY + 16.0 + firstIconRenderBox.size.height,
4782 ),
4783 ),
4784 );
4785
4786 nextDestinationY +=
4787 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0;
4788 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4789 expect(
4790 secondIconRenderBox.localToGlobal(Offset.zero),
4791 equals(Offset((142.0 - secondIconRenderBox.size.width) / 2.0, nextDestinationY + 24.0)),
4792 );
4793
4794 nextDestinationY += 72.0;
4795 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4796 expect(
4797 thirdIconRenderBox.localToGlobal(Offset.zero),
4798 equals(Offset((142.0 - thirdIconRenderBox.size.width) / 2.0, nextDestinationY + 24.0)),
4799 );
4800
4801 nextDestinationY += 72.0;
4802 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4803 expect(
4804 fourthIconRenderBox.localToGlobal(Offset.zero),
4805 equals(Offset((142.0 - fourthIconRenderBox.size.width) / 2.0, nextDestinationY + 24.0)),
4806 );
4807 });
4808
4809 testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (
4810 WidgetTester tester,
4811 ) async {
4812 await _pumpNavigationRail(
4813 tester,
4814 useMaterial3: false,
4815 textScaleFactor: 0.75,
4816 navigationRail: NavigationRail(
4817 selectedIndex: 0,
4818 destinations: _destinations(),
4819 labelType: NavigationRailLabelType.selected,
4820 ),
4821 );
4822
4823 // A smaller textScaleFactor will not reduce the default width of the rail
4824 // since there is a minWidth.
4825 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4826 expect(renderBox.size.width, 72.0);
4827
4828 // The first destination is 8 from the top because of the default vertical
4829 // padding at the to of the rail.
4830 double nextDestinationY = 8.0;
4831 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4832 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
4833 expect(
4834 firstIconRenderBox.localToGlobal(Offset.zero),
4835 equals(
4836 Offset(
4837 (72.0 - firstIconRenderBox.size.width) / 2.0,
4838 nextDestinationY +
4839 (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
4840 ),
4841 ),
4842 );
4843 expect(
4844 firstLabelRenderBox.localToGlobal(Offset.zero),
4845 equals(
4846 Offset(
4847 (72.0 - firstLabelRenderBox.size.width) / 2.0,
4848 nextDestinationY +
4849 (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
4850 ),
4851 ),
4852 );
4853
4854 // The second destination is 72 below the first destination.
4855 nextDestinationY += 72.0;
4856 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4857 expect(
4858 secondIconRenderBox.localToGlobal(Offset.zero),
4859 equals(
4860 Offset(
4861 (72.0 - secondIconRenderBox.size.width) / 2.0,
4862 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
4863 ),
4864 ),
4865 );
4866
4867 // The third destination is 72 below the second destination.
4868 nextDestinationY += 72.0;
4869 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4870 expect(
4871 thirdIconRenderBox.localToGlobal(Offset.zero),
4872 equals(
4873 Offset(
4874 (72.0 - thirdIconRenderBox.size.width) / 2.0,
4875 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
4876 ),
4877 ),
4878 );
4879
4880 // The fourth destination is 72 below the third destination.
4881 nextDestinationY += 72.0;
4882 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4883 expect(
4884 fourthIconRenderBox.localToGlobal(Offset.zero),
4885 equals(
4886 Offset(
4887 (72.0 - fourthIconRenderBox.size.width) / 2.0,
4888 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
4889 ),
4890 ),
4891 );
4892 });
4893
4894 testWidgets(
4895 'Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)',
4896 (WidgetTester tester) async {
4897 await _pumpNavigationRail(
4898 tester,
4899 useMaterial3: false,
4900 navigationRail: NavigationRail(
4901 selectedIndex: 0,
4902 destinations: _destinations(),
4903 labelType: NavigationRailLabelType.all,
4904 ),
4905 );
4906
4907 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
4908 expect(renderBox.size.width, 72.0);
4909
4910 // The first destination is 8 from the top because of the default vertical
4911 // padding at the to of the rail.
4912 double nextDestinationY = 8.0;
4913 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
4914 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
4915 expect(
4916 firstIconRenderBox.localToGlobal(Offset.zero),
4917 equals(Offset((72.0 - firstIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
4918 );
4919 expect(
4920 firstLabelRenderBox.localToGlobal(Offset.zero),
4921 equals(
4922 Offset(
4923 (72.0 - firstLabelRenderBox.size.width) / 2.0,
4924 nextDestinationY + 16.0 + firstIconRenderBox.size.height,
4925 ),
4926 ),
4927 );
4928
4929 // The second destination is 72 below the first destination.
4930 nextDestinationY += 72.0;
4931 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
4932 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
4933 expect(
4934 secondIconRenderBox.localToGlobal(Offset.zero),
4935 equals(Offset((72.0 - secondIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
4936 );
4937 expect(
4938 secondLabelRenderBox.localToGlobal(Offset.zero),
4939 equals(
4940 Offset(
4941 (72.0 - secondLabelRenderBox.size.width) / 2.0,
4942 nextDestinationY + 16.0 + secondIconRenderBox.size.height,
4943 ),
4944 ),
4945 );
4946
4947 // The third destination is 72 below the second destination.
4948 nextDestinationY += 72.0;
4949 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
4950 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
4951 expect(
4952 thirdIconRenderBox.localToGlobal(Offset.zero),
4953 equals(Offset((72.0 - thirdIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
4954 );
4955 expect(
4956 thirdLabelRenderBox.localToGlobal(Offset.zero),
4957 equals(
4958 Offset(
4959 (72.0 - thirdLabelRenderBox.size.width) / 2.0,
4960 nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
4961 ),
4962 ),
4963 );
4964
4965 // The fourth destination is 72 below the third destination.
4966 nextDestinationY += 72.0;
4967 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
4968 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
4969 expect(
4970 fourthIconRenderBox.localToGlobal(Offset.zero),
4971 equals(Offset((72.0 - fourthIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
4972 );
4973 expect(
4974 fourthLabelRenderBox.localToGlobal(Offset.zero),
4975 equals(
4976 Offset(
4977 (72.0 - fourthLabelRenderBox.size.width) / 2.0,
4978 nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
4979 ),
4980 ),
4981 );
4982 },
4983 );
4984
4985 testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (
4986 WidgetTester tester,
4987 ) async {
4988 await _pumpNavigationRail(
4989 tester,
4990 useMaterial3: false,
4991 textScaleFactor: 3.0,
4992 navigationRail: NavigationRail(
4993 selectedIndex: 0,
4994 destinations: _destinations(),
4995 labelType: NavigationRailLabelType.all,
4996 ),
4997 );
4998
4999 // The rail and destinations sizes grow to fit the larger text labels.
5000 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
5001 expect(renderBox.size.width, 142.0);
5002
5003 // The first destination is 8 from the top because of the default vertical
5004 // padding at the to of the rail.
5005 double nextDestinationY = 8.0;
5006 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5007 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
5008 expect(
5009 firstIconRenderBox.localToGlobal(Offset.zero),
5010 equals(Offset((142.0 - firstIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5011 );
5012 expect(
5013 firstLabelRenderBox.localToGlobal(Offset.zero),
5014 equals(
5015 Offset(
5016 (142.0 - firstLabelRenderBox.size.width) / 2.0,
5017 nextDestinationY + 16.0 + firstIconRenderBox.size.height,
5018 ),
5019 ),
5020 );
5021
5022 nextDestinationY +=
5023 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0;
5024 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5025 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
5026 expect(
5027 secondIconRenderBox.localToGlobal(Offset.zero),
5028 equals(Offset((142.0 - secondIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5029 );
5030 expect(
5031 secondLabelRenderBox.localToGlobal(Offset.zero),
5032 equals(
5033 Offset(
5034 (142.0 - secondLabelRenderBox.size.width) / 2.0,
5035 nextDestinationY + 16.0 + secondIconRenderBox.size.height,
5036 ),
5037 ),
5038 );
5039
5040 nextDestinationY +=
5041 16.0 + secondIconRenderBox.size.height + secondLabelRenderBox.size.height + 16.0;
5042 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5043 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
5044 expect(
5045 thirdIconRenderBox.localToGlobal(Offset.zero),
5046 equals(Offset((142.0 - thirdIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5047 );
5048 expect(
5049 thirdLabelRenderBox.localToGlobal(Offset.zero),
5050 equals(
5051 Offset(
5052 (142.0 - thirdLabelRenderBox.size.width) / 2.0,
5053 nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
5054 ),
5055 ),
5056 );
5057
5058 nextDestinationY +=
5059 16.0 + thirdIconRenderBox.size.height + thirdLabelRenderBox.size.height + 16.0;
5060 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5061 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
5062 expect(
5063 fourthIconRenderBox.localToGlobal(Offset.zero),
5064 equals(Offset((142.0 - fourthIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5065 );
5066 expect(
5067 fourthLabelRenderBox.localToGlobal(Offset.zero),
5068 equals(
5069 Offset(
5070 (142.0 - fourthLabelRenderBox.size.width) / 2.0,
5071 nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
5072 ),
5073 ),
5074 );
5075 });
5076
5077 testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (
5078 WidgetTester tester,
5079 ) async {
5080 await _pumpNavigationRail(
5081 tester,
5082 useMaterial3: false,
5083 textScaleFactor: 0.75,
5084 navigationRail: NavigationRail(
5085 selectedIndex: 0,
5086 destinations: _destinations(),
5087 labelType: NavigationRailLabelType.all,
5088 ),
5089 );
5090
5091 // A smaller textScaleFactor will not reduce the default size of the rail.
5092 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
5093 expect(renderBox.size.width, 72.0);
5094
5095 // The first destination is 8 from the top because of the default vertical
5096 // padding at the to of the rail.
5097 double nextDestinationY = 8.0;
5098 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5099 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
5100 expect(
5101 firstIconRenderBox.localToGlobal(Offset.zero),
5102 equals(Offset((72.0 - firstIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5103 );
5104 expect(
5105 firstLabelRenderBox.localToGlobal(Offset.zero),
5106 equals(
5107 Offset(
5108 (72.0 - firstLabelRenderBox.size.width) / 2.0,
5109 nextDestinationY + 16.0 + firstIconRenderBox.size.height,
5110 ),
5111 ),
5112 );
5113
5114 // The second destination is 72 below the first destination.
5115 nextDestinationY += 72.0;
5116 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5117 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
5118 expect(
5119 secondIconRenderBox.localToGlobal(Offset.zero),
5120 equals(Offset((72.0 - secondIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5121 );
5122 expect(
5123 secondLabelRenderBox.localToGlobal(Offset.zero),
5124 equals(
5125 Offset(
5126 (72.0 - secondLabelRenderBox.size.width) / 2.0,
5127 nextDestinationY + 16.0 + secondIconRenderBox.size.height,
5128 ),
5129 ),
5130 );
5131
5132 // The third destination is 72 below the second destination.
5133 nextDestinationY += 72.0;
5134 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5135 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
5136 expect(
5137 thirdIconRenderBox.localToGlobal(Offset.zero),
5138 equals(Offset((72.0 - thirdIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5139 );
5140 expect(
5141 thirdLabelRenderBox.localToGlobal(Offset.zero),
5142 equals(
5143 Offset(
5144 (72.0 - thirdLabelRenderBox.size.width) / 2.0,
5145 nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
5146 ),
5147 ),
5148 );
5149
5150 // The fourth destination is 72 below the third destination.
5151 nextDestinationY += 72.0;
5152 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5153 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
5154 expect(
5155 fourthIconRenderBox.localToGlobal(Offset.zero),
5156 equals(Offset((72.0 - fourthIconRenderBox.size.width) / 2.0, nextDestinationY + 16.0)),
5157 );
5158 expect(
5159 fourthLabelRenderBox.localToGlobal(Offset.zero),
5160 equals(
5161 Offset(
5162 (72.0 - fourthLabelRenderBox.size.width) / 2.0,
5163 nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
5164 ),
5165 ),
5166 );
5167 });
5168
5169 testWidgets(
5170 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)',
5171 (WidgetTester tester) async {
5172 await _pumpNavigationRail(
5173 tester,
5174 useMaterial3: false,
5175 navigationRail: NavigationRail(
5176 selectedIndex: 0,
5177 minWidth: 56.0,
5178 destinations: _destinations(),
5179 ),
5180 );
5181
5182 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
5183 expect(renderBox.size.width, 56.0);
5184
5185 // The first destination is 8 from the top because of the default vertical
5186 // padding at the to of the rail.
5187 double nextDestinationY = 8.0;
5188 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5189 expect(
5190 firstIconRenderBox.localToGlobal(Offset.zero),
5191 equals(
5192 Offset(
5193 (56.0 - firstIconRenderBox.size.width) / 2.0,
5194 nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
5195 ),
5196 ),
5197 );
5198
5199 // The second destination is 56 below the first destination.
5200 nextDestinationY += 56.0;
5201 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5202 expect(
5203 secondIconRenderBox.localToGlobal(Offset.zero),
5204 equals(
5205 Offset(
5206 (56.0 - secondIconRenderBox.size.width) / 2.0,
5207 nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
5208 ),
5209 ),
5210 );
5211
5212 // The third destination is 56 below the second destination.
5213 nextDestinationY += 56.0;
5214 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5215 expect(
5216 thirdIconRenderBox.localToGlobal(Offset.zero),
5217 equals(
5218 Offset(
5219 (56.0 - thirdIconRenderBox.size.width) / 2.0,
5220 nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
5221 ),
5222 ),
5223 );
5224
5225 // The fourth destination is 56 below the third destination.
5226 nextDestinationY += 56.0;
5227 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5228 expect(
5229 fourthIconRenderBox.localToGlobal(Offset.zero),
5230 equals(
5231 Offset(
5232 (56.0 - fourthIconRenderBox.size.width) / 2.0,
5233 nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
5234 ),
5235 ),
5236 );
5237 },
5238 );
5239
5240 testWidgets(
5241 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0',
5242 (WidgetTester tester) async {
5243 await _pumpNavigationRail(
5244 tester,
5245 useMaterial3: false,
5246 textScaleFactor: 3.0,
5247 navigationRail: NavigationRail(
5248 selectedIndex: 0,
5249 minWidth: 56.0,
5250 destinations: _destinations(),
5251 ),
5252 );
5253
5254 // Since the rail is icon only, its preferred width should not be affected
5255 // by textScaleFactor.
5256 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
5257 expect(renderBox.size.width, 56.0);
5258
5259 // The first destination is 8 from the top because of the default vertical
5260 // padding at the to of the rail.
5261 double nextDestinationY = 8.0;
5262 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5263 expect(
5264 firstIconRenderBox.localToGlobal(Offset.zero),
5265 equals(
5266 Offset(
5267 (56.0 - firstIconRenderBox.size.width) / 2.0,
5268 nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
5269 ),
5270 ),
5271 );
5272
5273 // The second destination is 56 below the first destination.
5274 nextDestinationY += 56.0;
5275 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5276 expect(
5277 secondIconRenderBox.localToGlobal(Offset.zero),
5278 equals(
5279 Offset(
5280 (56.0 - secondIconRenderBox.size.width) / 2.0,
5281 nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
5282 ),
5283 ),
5284 );
5285
5286 // The third destination is 56 below the second destination.
5287 nextDestinationY += 56.0;
5288 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5289 expect(
5290 thirdIconRenderBox.localToGlobal(Offset.zero),
5291 equals(
5292 Offset(
5293 (56.0 - thirdIconRenderBox.size.width) / 2.0,
5294 nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
5295 ),
5296 ),
5297 );
5298
5299 // The fourth destination is 56 below the third destination.
5300 nextDestinationY += 56.0;
5301 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5302 expect(
5303 fourthIconRenderBox.localToGlobal(Offset.zero),
5304 equals(
5305 Offset(
5306 (56.0 - fourthIconRenderBox.size.width) / 2.0,
5307 nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
5308 ),
5309 ),
5310 );
5311 },
5312 );
5313
5314 testWidgets(
5315 'Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75',
5316 (WidgetTester tester) async {
5317 await _pumpNavigationRail(
5318 tester,
5319 useMaterial3: false,
5320 textScaleFactor: 3.0,
5321 navigationRail: NavigationRail(
5322 selectedIndex: 0,
5323 minWidth: 56.0,
5324 destinations: _destinations(),
5325 ),
5326 );
5327
5328 // Since the rail is icon only, its preferred width should not be affected
5329 // by textScaleFactor.
5330 final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
5331 expect(renderBox.size.width, 56.0);
5332
5333 // The first destination is 8 from the top because of the default vertical
5334 // padding at the to of the rail.
5335 double nextDestinationY = 8.0;
5336 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5337 expect(
5338 firstIconRenderBox.localToGlobal(Offset.zero),
5339 equals(
5340 Offset(
5341 (56.0 - firstIconRenderBox.size.width) / 2.0,
5342 nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
5343 ),
5344 ),
5345 );
5346
5347 // The second destination is 56 below the first destination.
5348 nextDestinationY += 56.0;
5349 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5350 expect(
5351 secondIconRenderBox.localToGlobal(Offset.zero),
5352 equals(
5353 Offset(
5354 (56.0 - secondIconRenderBox.size.width) / 2.0,
5355 nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
5356 ),
5357 ),
5358 );
5359
5360 // The third destination is 56 below the second destination.
5361 nextDestinationY += 56.0;
5362 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5363 expect(
5364 thirdIconRenderBox.localToGlobal(Offset.zero),
5365 equals(
5366 Offset(
5367 (56.0 - thirdIconRenderBox.size.width) / 2.0,
5368 nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
5369 ),
5370 ),
5371 );
5372
5373 // The fourth destination is 56 below the third destination.
5374 nextDestinationY += 56.0;
5375 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5376 expect(
5377 fourthIconRenderBox.localToGlobal(Offset.zero),
5378 equals(
5379 Offset(
5380 (56.0 - fourthIconRenderBox.size.width) / 2.0,
5381 nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
5382 ),
5383 ),
5384 );
5385 },
5386 );
5387
5388 testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (
5389 WidgetTester tester,
5390 ) async {
5391 await _pumpNavigationRail(
5392 tester,
5393 useMaterial3: false,
5394 navigationRail: NavigationRail(selectedIndex: 0, destinations: _destinations()),
5395 );
5396
5397 // The first destination is 8 from the top because of the default vertical
5398 // padding at the to of the rail.
5399 double nextDestinationY = 8.0;
5400 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5401 expect(
5402 firstIconRenderBox.localToGlobal(Offset.zero),
5403 equals(
5404 Offset(
5405 (72.0 - firstIconRenderBox.size.width) / 2.0,
5406 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
5407 ),
5408 ),
5409 );
5410
5411 // The second destination is 72 below the first destination.
5412 nextDestinationY += 72.0;
5413 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5414 expect(
5415 secondIconRenderBox.localToGlobal(Offset.zero),
5416 equals(
5417 Offset(
5418 (72.0 - secondIconRenderBox.size.width) / 2.0,
5419 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
5420 ),
5421 ),
5422 );
5423
5424 // The third destination is 72 below the second destination.
5425 nextDestinationY += 72.0;
5426 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5427 expect(
5428 thirdIconRenderBox.localToGlobal(Offset.zero),
5429 equals(
5430 Offset(
5431 (72.0 - thirdIconRenderBox.size.width) / 2.0,
5432 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
5433 ),
5434 ),
5435 );
5436
5437 // The fourth destination is 72 below the third destination.
5438 nextDestinationY += 72.0;
5439 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5440 expect(
5441 fourthIconRenderBox.localToGlobal(Offset.zero),
5442 equals(
5443 Offset(
5444 (72.0 - fourthIconRenderBox.size.width) / 2.0,
5445 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
5446 ),
5447 ),
5448 );
5449 });
5450
5451 testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async {
5452 await _pumpNavigationRail(
5453 tester,
5454 useMaterial3: false,
5455 navigationRail: NavigationRail(
5456 selectedIndex: 0,
5457 groupAlignment: 0.0,
5458 destinations: _destinations(),
5459 ),
5460 );
5461
5462 double nextDestinationY = 160.0;
5463 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5464 expect(
5465 firstIconRenderBox.localToGlobal(Offset.zero),
5466 equals(
5467 Offset(
5468 (72.0 - firstIconRenderBox.size.width) / 2.0,
5469 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
5470 ),
5471 ),
5472 );
5473
5474 // The second destination is 72 below the first destination.
5475 nextDestinationY += 72.0;
5476 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5477 expect(
5478 secondIconRenderBox.localToGlobal(Offset.zero),
5479 equals(
5480 Offset(
5481 (72.0 - secondIconRenderBox.size.width) / 2.0,
5482 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
5483 ),
5484 ),
5485 );
5486
5487 // The third destination is 72 below the second destination.
5488 nextDestinationY += 72.0;
5489 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5490 expect(
5491 thirdIconRenderBox.localToGlobal(Offset.zero),
5492 equals(
5493 Offset(
5494 (72.0 - thirdIconRenderBox.size.width) / 2.0,
5495 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
5496 ),
5497 ),
5498 );
5499
5500 // The fourth destination is 72 below the third destination.
5501 nextDestinationY += 72.0;
5502 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5503 expect(
5504 fourthIconRenderBox.localToGlobal(Offset.zero),
5505 equals(
5506 Offset(
5507 (72.0 - fourthIconRenderBox.size.width) / 2.0,
5508 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
5509 ),
5510 ),
5511 );
5512 });
5513
5514 testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async {
5515 await _pumpNavigationRail(
5516 tester,
5517 useMaterial3: false,
5518 navigationRail: NavigationRail(
5519 selectedIndex: 0,
5520 groupAlignment: 1.0,
5521 destinations: _destinations(),
5522 ),
5523 );
5524
5525 double nextDestinationY = 312.0;
5526 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5527 expect(
5528 firstIconRenderBox.localToGlobal(Offset.zero),
5529 equals(
5530 Offset(
5531 (72.0 - firstIconRenderBox.size.width) / 2.0,
5532 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
5533 ),
5534 ),
5535 );
5536
5537 // The second destination is 72 below the first destination.
5538 nextDestinationY += 72.0;
5539 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5540 expect(
5541 secondIconRenderBox.localToGlobal(Offset.zero),
5542 equals(
5543 Offset(
5544 (72.0 - secondIconRenderBox.size.width) / 2.0,
5545 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
5546 ),
5547 ),
5548 );
5549
5550 // The third destination is 72 below the second destination.
5551 nextDestinationY += 72.0;
5552 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5553 expect(
5554 thirdIconRenderBox.localToGlobal(Offset.zero),
5555 equals(
5556 Offset(
5557 (72.0 - thirdIconRenderBox.size.width) / 2.0,
5558 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
5559 ),
5560 ),
5561 );
5562
5563 // The fourth destination is 72 below the third destination.
5564 nextDestinationY += 72.0;
5565 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5566 expect(
5567 fourthIconRenderBox.localToGlobal(Offset.zero),
5568 equals(
5569 Offset(
5570 (72.0 - fourthIconRenderBox.size.width) / 2.0,
5571 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
5572 ),
5573 ),
5574 );
5575 });
5576
5577 testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async {
5578 await _pumpNavigationRail(
5579 tester,
5580 useMaterial3: false,
5581 navigationRail: NavigationRail(
5582 selectedIndex: 0,
5583 leading: FloatingActionButton(onPressed: () {}),
5584 trailing: FloatingActionButton(onPressed: () {}),
5585 destinations: _destinations(),
5586 ),
5587 );
5588
5589 final RenderBox leading = tester.renderObject<RenderBox>(
5590 find.byType(FloatingActionButton).at(0),
5591 );
5592 final RenderBox trailing = tester.renderObject<RenderBox>(
5593 find.byType(FloatingActionButton).at(1),
5594 );
5595 expect(leading.localToGlobal(Offset.zero), Offset((72 - leading.size.width) / 2.0, 8.0));
5596 expect(trailing.localToGlobal(Offset.zero), Offset((72 - trailing.size.width) / 2.0, 360.0));
5597 });
5598
5599 testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (
5600 WidgetTester tester,
5601 ) async {
5602 bool extended = false;
5603 late StateSetter stateSetter;
5604
5605 await tester.pumpWidget(
5606 MaterialApp(
5607 theme: ThemeData(useMaterial3: false),
5608 home: StatefulBuilder(
5609 builder: (BuildContext context, StateSetter setState) {
5610 stateSetter = setState;
5611 return Scaffold(
5612 body: Row(
5613 children: <Widget>[
5614 NavigationRail(
5615 selectedIndex: 0,
5616 destinations: _destinations(),
5617 extended: extended,
5618 ),
5619 const Expanded(child: Text('body')),
5620 ],
5621 ),
5622 );
5623 },
5624 ),
5625 ),
5626 );
5627
5628 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
5629
5630 expect(rail.size.width, equals(72.0));
5631
5632 stateSetter(() {
5633 extended = true;
5634 });
5635
5636 await tester.pump();
5637 await tester.pump(const Duration(milliseconds: 100));
5638 expect(rail.size.width, equals(164.0));
5639
5640 await tester.pumpAndSettle();
5641 expect(rail.size.width, equals(256.0));
5642
5643 // The first destination is 8 from the top because of the default vertical
5644 // padding at the to of the rail.
5645 double nextDestinationY = 8.0;
5646 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5647 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
5648 expect(
5649 firstIconRenderBox.localToGlobal(Offset.zero),
5650 equals(
5651 Offset(
5652 (72.0 - firstIconRenderBox.size.width) / 2.0,
5653 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
5654 ),
5655 ),
5656 );
5657 expect(
5658 firstLabelRenderBox.localToGlobal(Offset.zero),
5659 equals(Offset(72.0, nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0)),
5660 );
5661
5662 // The second destination is 72 below the first destination.
5663 nextDestinationY += 72.0;
5664 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5665 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
5666 expect(
5667 secondIconRenderBox.localToGlobal(Offset.zero),
5668 equals(
5669 Offset(
5670 (72.0 - secondIconRenderBox.size.width) / 2.0,
5671 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
5672 ),
5673 ),
5674 );
5675 expect(
5676 secondLabelRenderBox.localToGlobal(Offset.zero),
5677 equals(Offset(72.0, nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0)),
5678 );
5679
5680 // The third destination is 72 below the second destination.
5681 nextDestinationY += 72.0;
5682 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5683 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
5684 expect(
5685 thirdIconRenderBox.localToGlobal(Offset.zero),
5686 equals(
5687 Offset(
5688 (72.0 - thirdIconRenderBox.size.width) / 2.0,
5689 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
5690 ),
5691 ),
5692 );
5693 expect(
5694 thirdLabelRenderBox.localToGlobal(Offset.zero),
5695 equals(Offset(72.0, nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0)),
5696 );
5697
5698 // The fourth destination is 72 below the third destination.
5699 nextDestinationY += 72.0;
5700 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5701 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
5702 expect(
5703 fourthIconRenderBox.localToGlobal(Offset.zero),
5704 equals(
5705 Offset(
5706 (72.0 - fourthIconRenderBox.size.width) / 2.0,
5707 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
5708 ),
5709 ),
5710 );
5711 expect(
5712 fourthLabelRenderBox.localToGlobal(Offset.zero),
5713 equals(Offset(72.0, nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0)),
5714 );
5715 });
5716
5717 testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (
5718 WidgetTester tester,
5719 ) async {
5720 bool extended = false;
5721 late StateSetter stateSetter;
5722
5723 await tester.pumpWidget(
5724 MaterialApp(
5725 theme: ThemeData(useMaterial3: false),
5726 home: StatefulBuilder(
5727 builder: (BuildContext context, StateSetter setState) {
5728 stateSetter = setState;
5729 return Directionality(
5730 textDirection: TextDirection.rtl,
5731 child: Scaffold(
5732 body: Row(
5733 textDirection: TextDirection.rtl,
5734 children: <Widget>[
5735 NavigationRail(
5736 selectedIndex: 0,
5737 destinations: _destinations(),
5738 extended: extended,
5739 ),
5740 const Expanded(child: Text('body')),
5741 ],
5742 ),
5743 ),
5744 );
5745 },
5746 ),
5747 ),
5748 );
5749
5750 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
5751
5752 expect(rail.size.width, equals(72.0));
5753 expect(rail.localToGlobal(Offset.zero), equals(const Offset(728.0, 0.0)));
5754
5755 stateSetter(() {
5756 extended = true;
5757 });
5758
5759 await tester.pump();
5760 await tester.pump(const Duration(milliseconds: 100));
5761 expect(rail.size.width, equals(164.0));
5762 expect(rail.localToGlobal(Offset.zero), equals(const Offset(636.0, 0.0)));
5763
5764 await tester.pumpAndSettle();
5765 expect(rail.size.width, equals(256.0));
5766 expect(rail.localToGlobal(Offset.zero), equals(const Offset(544.0, 0.0)));
5767
5768 // The first destination is 8 from the top because of the default vertical
5769 // padding at the to of the rail.
5770 double nextDestinationY = 8.0;
5771 final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
5772 final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
5773 expect(
5774 firstIconRenderBox.localToGlobal(Offset.zero),
5775 equals(
5776 Offset(
5777 800.0 - (72.0 + firstIconRenderBox.size.width) / 2.0,
5778 nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
5779 ),
5780 ),
5781 );
5782 expect(
5783 firstLabelRenderBox.localToGlobal(Offset.zero),
5784 equals(
5785 Offset(
5786 800.0 - 72.0 - firstLabelRenderBox.size.width,
5787 nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0,
5788 ),
5789 ),
5790 );
5791
5792 // The second destination is 72 below the first destination.
5793 nextDestinationY += 72.0;
5794 final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
5795 final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
5796 expect(
5797 secondIconRenderBox.localToGlobal(Offset.zero),
5798 equals(
5799 Offset(
5800 800.0 - (72.0 + secondIconRenderBox.size.width) / 2.0,
5801 nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
5802 ),
5803 ),
5804 );
5805 expect(
5806 secondLabelRenderBox.localToGlobal(Offset.zero),
5807 equals(
5808 Offset(
5809 800.0 - 72.0 - secondLabelRenderBox.size.width,
5810 nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0,
5811 ),
5812 ),
5813 );
5814
5815 // The third destination is 72 below the second destination.
5816 nextDestinationY += 72.0;
5817 final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
5818 final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
5819 expect(
5820 thirdIconRenderBox.localToGlobal(Offset.zero),
5821 equals(
5822 Offset(
5823 800.0 - (72.0 + thirdIconRenderBox.size.width) / 2.0,
5824 nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
5825 ),
5826 ),
5827 );
5828 expect(
5829 thirdLabelRenderBox.localToGlobal(Offset.zero),
5830 equals(
5831 Offset(
5832 800.0 - 72.0 - thirdLabelRenderBox.size.width,
5833 nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0,
5834 ),
5835 ),
5836 );
5837
5838 // The fourth destination is 72 below the third destination.
5839 nextDestinationY += 72.0;
5840 final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
5841 final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
5842 expect(
5843 fourthIconRenderBox.localToGlobal(Offset.zero),
5844 equals(
5845 Offset(
5846 800.0 - (72.0 + fourthIconRenderBox.size.width) / 2.0,
5847 nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
5848 ),
5849 ),
5850 );
5851 expect(
5852 fourthLabelRenderBox.localToGlobal(Offset.zero),
5853 equals(
5854 Offset(
5855 800.0 - 72.0 - fourthLabelRenderBox.size.width,
5856 nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0,
5857 ),
5858 ),
5859 );
5860 });
5861
5862 testWidgets('Extended rail gets wider with longer labels are larger text scale', (
5863 WidgetTester tester,
5864 ) async {
5865 bool extended = false;
5866 late StateSetter stateSetter;
5867
5868 await tester.pumpWidget(
5869 MaterialApp(
5870 theme: ThemeData(useMaterial3: false),
5871 home: StatefulBuilder(
5872 builder: (BuildContext context, StateSetter setState) {
5873 stateSetter = setState;
5874 return Scaffold(
5875 body: Row(
5876 children: <Widget>[
5877 MediaQuery.withClampedTextScaling(
5878 minScaleFactor: 3.0,
5879 maxScaleFactor: 3.0,
5880 child: NavigationRail(
5881 selectedIndex: 0,
5882 destinations: const <NavigationRailDestination>[
5883 NavigationRailDestination(
5884 icon: Icon(Icons.favorite_border),
5885 selectedIcon: Icon(Icons.favorite),
5886 label: Text('Abc'),
5887 ),
5888 NavigationRailDestination(
5889 icon: Icon(Icons.bookmark_border),
5890 selectedIcon: Icon(Icons.bookmark),
5891 label: Text('Longer Label'),
5892 ),
5893 ],
5894 extended: extended,
5895 ),
5896 ),
5897 const Expanded(child: Text('body')),
5898 ],
5899 ),
5900 );
5901 },
5902 ),
5903 ),
5904 );
5905
5906 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
5907
5908 expect(rail.size.width, equals(72.0));
5909
5910 stateSetter(() {
5911 extended = true;
5912 });
5913
5914 await tester.pump();
5915 await tester.pump(const Duration(milliseconds: 100));
5916 expect(rail.size.width, equals(328.0));
5917
5918 await tester.pumpAndSettle();
5919 expect(rail.size.width, equals(584.0));
5920 });
5921
5922 testWidgets('Extended rail final width can be changed', (WidgetTester tester) async {
5923 bool extended = false;
5924 late StateSetter stateSetter;
5925
5926 await tester.pumpWidget(
5927 MaterialApp(
5928 theme: ThemeData(useMaterial3: false),
5929 home: StatefulBuilder(
5930 builder: (BuildContext context, StateSetter setState) {
5931 stateSetter = setState;
5932 return Scaffold(
5933 body: Row(
5934 children: <Widget>[
5935 NavigationRail(
5936 selectedIndex: 0,
5937 minExtendedWidth: 300,
5938 destinations: _destinations(),
5939 extended: extended,
5940 ),
5941 const Expanded(child: Text('body')),
5942 ],
5943 ),
5944 );
5945 },
5946 ),
5947 ),
5948 );
5949
5950 final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
5951
5952 expect(rail.size.width, equals(72.0));
5953
5954 stateSetter(() {
5955 extended = true;
5956 });
5957
5958 await tester.pumpAndSettle();
5959 expect(rail.size.width, equals(300.0));
5960 });
5961
5962 /// Regression test for https://github.com/flutter/flutter/issues/65657
5963 testWidgets('Extended rail transition does not jump from the beginning', (
5964 WidgetTester tester,
5965 ) async {
5966 bool extended = false;
5967 late StateSetter stateSetter;
5968
5969 await tester.pumpWidget(
5970 MaterialApp(
5971 theme: ThemeData(useMaterial3: false),
5972 home: StatefulBuilder(
5973 builder: (BuildContext context, StateSetter setState) {
5974 stateSetter = setState;
5975 return Scaffold(
5976 body: Row(
5977 children: <Widget>[
5978 NavigationRail(
5979 selectedIndex: 0,
5980 destinations: const <NavigationRailDestination>[
5981 NavigationRailDestination(
5982 icon: Icon(Icons.favorite_border),
5983 selectedIcon: Icon(Icons.favorite),
5984 label: Text('Abc'),
5985 ),
5986 NavigationRailDestination(
5987 icon: Icon(Icons.bookmark_border),
5988 selectedIcon: Icon(Icons.bookmark),
5989 label: Text('Longer Label'),
5990 ),
5991 ],
5992 extended: extended,
5993 ),
5994 const Expanded(child: Text('body')),
5995 ],
5996 ),
5997 );
5998 },
5999 ),
6000 ),
6001 );
6002
6003 final Finder rail = find.byType(NavigationRail);
6004
6005 // Before starting the animation, the rail has a width of 72.
6006 expect(tester.getSize(rail).width, 72.0);
6007
6008 stateSetter(() {
6009 extended = true;
6010 });
6011
6012 await tester.pump();
6013 // Create very close to 0, but non-zero, animation value.
6014 await tester.pump(const Duration(milliseconds: 1));
6015 // Expect that it has started to extend.
6016 expect(tester.getSize(rail).width, greaterThan(72.0));
6017 // Expect that it has only extended by a small amount, or that the first
6018 // frame does not jump. This helps verify that it is a smooth animation.
6019 expect(tester.getSize(rail).width, closeTo(72.0, 1.0));
6020 });
6021
6022 testWidgets('NavigationRailDestination adds circular indicator when no labels are present', (
6023 WidgetTester tester,
6024 ) async {
6025 await _pumpNavigationRail(
6026 tester,
6027 useMaterial3: false,
6028 navigationRail: NavigationRail(
6029 useIndicator: true,
6030 labelType: NavigationRailLabelType.none,
6031 selectedIndex: 0,
6032 destinations: const <NavigationRailDestination>[
6033 NavigationRailDestination(
6034 icon: Icon(Icons.favorite_border),
6035 selectedIcon: Icon(Icons.favorite),
6036 label: Text('Abc'),
6037 ),
6038 NavigationRailDestination(
6039 icon: Icon(Icons.bookmark_border),
6040 selectedIcon: Icon(Icons.bookmark),
6041 label: Text('Def'),
6042 ),
6043 NavigationRailDestination(
6044 icon: Icon(Icons.star_border),
6045 selectedIcon: Icon(Icons.star),
6046 label: Text('Ghi'),
6047 ),
6048 ],
6049 ),
6050 );
6051
6052 final NavigationIndicator indicator = tester.widget<NavigationIndicator>(
6053 find.byType(NavigationIndicator).first,
6054 );
6055
6056 expect(indicator.width, 56);
6057 expect(indicator.height, 56);
6058 });
6059
6060 testWidgets('NavigationRailDestination has center aligned indicator - [labelType]=none', (
6061 WidgetTester tester,
6062 ) async {
6063 // This is a regression test for
6064 // https://github.com/flutter/flutter/issues/97753
6065 await _pumpNavigationRail(
6066 tester,
6067 useMaterial3: false,
6068 navigationRail: NavigationRail(
6069 labelType: NavigationRailLabelType.none,
6070 selectedIndex: 0,
6071 destinations: const <NavigationRailDestination>[
6072 NavigationRailDestination(
6073 icon: Stack(
6074 children: <Widget>[
6075 Icon(Icons.umbrella),
6076 Positioned(
6077 top: 0,
6078 right: 0,
6079 child: Text('Text', style: TextStyle(fontSize: 10, color: Colors.red)),
6080 ),
6081 ],
6082 ),
6083 label: Text('Abc'),
6084 ),
6085 NavigationRailDestination(icon: Icon(Icons.umbrella), label: Text('Def')),
6086 NavigationRailDestination(icon: Icon(Icons.bookmark_border), label: Text('Ghi')),
6087 ],
6088 ),
6089 );
6090 // Indicator with Stack widget
6091 final RenderBox firstIndicator = tester.renderObject(find.byType(Icon).first);
6092 expect(firstIndicator.localToGlobal(Offset.zero).dx, 24.0);
6093 // Indicator without Stack widget
6094 final RenderBox lastIndicator = tester.renderObject(find.byType(Icon).last);
6095 expect(lastIndicator.localToGlobal(Offset.zero).dx, 24.0);
6096 });
6097
6098 testWidgets('NavigationRail respects the notch/system navigation bar in landscape mode', (
6099 WidgetTester tester,
6100 ) async {
6101 const double safeAreaPadding = 40.0;
6102 NavigationRail navigationRail() {
6103 return NavigationRail(
6104 selectedIndex: 0,
6105 destinations: const <NavigationRailDestination>[
6106 NavigationRailDestination(
6107 icon: Icon(Icons.favorite_border),
6108 selectedIcon: Icon(Icons.favorite),
6109 label: Text('Abc'),
6110 ),
6111 NavigationRailDestination(
6112 icon: Icon(Icons.bookmark_border),
6113 selectedIcon: Icon(Icons.bookmark),
6114 label: Text('Def'),
6115 ),
6116 ],
6117 );
6118 }
6119
6120 await tester.pumpWidget(_buildWidget(navigationRail(), useMaterial3: false));
6121 final double defaultWidth = tester.getSize(find.byType(NavigationRail)).width;
6122 expect(defaultWidth, 72);
6123
6124 await tester.pumpWidget(
6125 _buildWidget(
6126 MediaQuery(
6127 data: const MediaQueryData(padding: EdgeInsets.only(left: safeAreaPadding)),
6128 child: navigationRail(),
6129 ),
6130 useMaterial3: false,
6131 ),
6132 );
6133 final double updatedWidth = tester.getSize(find.byType(NavigationRail)).width;
6134 expect(updatedWidth, defaultWidth + safeAreaPadding);
6135
6136 // test width when text direction is RTL.
6137 await tester.pumpWidget(
6138 _buildWidget(
6139 MediaQuery(
6140 data: const MediaQueryData(padding: EdgeInsets.only(right: safeAreaPadding)),
6141 child: navigationRail(),
6142 ),
6143 useMaterial3: false,
6144 isRTL: true,
6145 ),
6146 );
6147 final double updatedWidthRTL = tester.getSize(find.byType(NavigationRail)).width;
6148 expect(updatedWidthRTL, defaultWidth + safeAreaPadding);
6149 });
6150 }); // End Material 2 group
6151}
6152
6153TestSemantics _expectedSemantics({bool scrollable = false}) {
6154 List<TestSemantics> destinations = <TestSemantics>[
6155 TestSemantics(
6156 flags: <SemanticsFlag>[
6157 SemanticsFlag.hasSelectedState,
6158 SemanticsFlag.isSelected,
6159 SemanticsFlag.isFocusable,
6160 ],
6161 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
6162 label: 'Abc\nTab 1 of 4',
6163 textDirection: TextDirection.ltr,
6164 ),
6165 TestSemantics(
6166 flags: <SemanticsFlag>[SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState],
6167 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
6168 label: 'Def\nTab 2 of 4',
6169 textDirection: TextDirection.ltr,
6170 ),
6171 TestSemantics(
6172 flags: <SemanticsFlag>[SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState],
6173 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
6174 label: 'Ghi\nTab 3 of 4',
6175 textDirection: TextDirection.ltr,
6176 ),
6177 TestSemantics(
6178 flags: <SemanticsFlag>[SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState],
6179 actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
6180 label: 'Jkl\nTab 4 of 4',
6181 textDirection: TextDirection.ltr,
6182 ),
6183 ];
6184
6185 if (scrollable) {
6186 destinations = <TestSemantics>[
6187 TestSemantics(
6188 flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
6189 children: destinations,
6190 ),
6191 ];
6192 }
6193
6194 return TestSemantics.root(
6195 children: <TestSemantics>[
6196 TestSemantics(
6197 textDirection: TextDirection.ltr,
6198 children: <TestSemantics>[
6199 TestSemantics(
6200 children: <TestSemantics>[
6201 TestSemantics(
6202 flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
6203 children: <TestSemantics>[
6204 ...destinations,
6205 TestSemantics(label: 'body', textDirection: TextDirection.ltr),
6206 ],
6207 ),
6208 ],
6209 ),
6210 ],
6211 ),
6212 ],
6213 );
6214}
6215
6216List<NavigationRailDestination> _destinations() {
6217 return const <NavigationRailDestination>[
6218 NavigationRailDestination(
6219 icon: Icon(Icons.favorite_border),
6220 selectedIcon: Icon(Icons.favorite),
6221 label: Text('Abc'),
6222 ),
6223 NavigationRailDestination(
6224 icon: Icon(Icons.bookmark_border),
6225 selectedIcon: Icon(Icons.bookmark),
6226 label: Text('Def'),
6227 ),
6228 NavigationRailDestination(
6229 icon: Icon(Icons.star_border),
6230 selectedIcon: Icon(Icons.star),
6231 label: Text('Ghi'),
6232 ),
6233 NavigationRailDestination(
6234 icon: Icon(Icons.hotel),
6235 selectedIcon: Icon(Icons.home),
6236 label: Text('Jkl'),
6237 ),
6238 ];
6239}
6240
6241Future<void> _pumpNavigationRail(
6242 WidgetTester tester, {
6243 double textScaleFactor = 1.0,
6244 required NavigationRail navigationRail,
6245 bool useMaterial3 = true,
6246 NavigationRailThemeData? navigationRailTheme,
6247}) async {
6248 await tester.pumpWidget(
6249 MaterialApp(
6250 theme: ThemeData(useMaterial3: useMaterial3, navigationRailTheme: navigationRailTheme),
6251 home: Builder(
6252 builder: (BuildContext context) {
6253 return MediaQuery.withClampedTextScaling(
6254 minScaleFactor: textScaleFactor,
6255 maxScaleFactor: textScaleFactor,
6256 child: Scaffold(
6257 body: Row(
6258 children: <Widget>[
6259 navigationRail,
6260 const Expanded(child: Text('body')),
6261 ],
6262 ),
6263 ),
6264 );
6265 },
6266 ),
6267 ),
6268 );
6269}
6270
6271Future<void> _pumpLocalizedTestRail(
6272 WidgetTester tester, {
6273 NavigationRailLabelType? labelType,
6274 bool extended = false,
6275 bool scrollable = false,
6276}) async {
6277 await tester.pumpWidget(
6278 Localizations(
6279 locale: const Locale('en', 'US'),
6280 delegates: const <LocalizationsDelegate<dynamic>>[
6281 DefaultMaterialLocalizations.delegate,
6282 DefaultWidgetsLocalizations.delegate,
6283 ],
6284 child: MaterialApp(
6285 home: Scaffold(
6286 body: Row(
6287 children: <Widget>[
6288 NavigationRail(
6289 selectedIndex: 0,
6290 extended: extended,
6291 destinations: _destinations(),
6292 labelType: labelType,
6293 scrollable: scrollable,
6294 ),
6295 const Expanded(child: Text('body')),
6296 ],
6297 ),
6298 ),
6299 ),
6300 ),
6301 );
6302}
6303
6304RenderBox _iconRenderBox(WidgetTester tester, IconData iconData) {
6305 return tester.firstRenderObject<RenderBox>(
6306 find.descendant(of: find.byIcon(iconData), matching: find.byType(RichText)),
6307 );
6308}
6309
6310RenderBox _labelRenderBox(WidgetTester tester, String text) {
6311 return tester.firstRenderObject<RenderBox>(
6312 find.descendant(of: find.text(text), matching: find.byType(RichText)),
6313 );
6314}
6315
6316TextStyle _iconStyle(WidgetTester tester, IconData icon) {
6317 return tester
6318 .widget<RichText>(find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)))
6319 .text
6320 .style!;
6321}
6322
6323Finder _opacityAboveLabel(String text) {
6324 return find.ancestor(of: find.text(text), matching: find.byType(Opacity));
6325}
6326
6327// Only valid when labelType != all.
6328double? _labelOpacity(WidgetTester tester, String text) {
6329 // We search for both Visibility and FadeTransition since in some
6330 // cases opacity is animated, in other it's not.
6331 final Iterable<Visibility> visibilityWidgets = tester.widgetList<Visibility>(
6332 find.ancestor(of: find.text(text), matching: find.byType(Visibility)),
6333 );
6334 if (visibilityWidgets.isNotEmpty) {
6335 return visibilityWidgets.single.visible ? 1.0 : 0.0;
6336 }
6337
6338 final FadeTransition fadeTransitionWidget = tester.widget<FadeTransition>(
6339 find
6340 .ancestor(of: find.text(text), matching: find.byType(FadeTransition))
6341 .first, // first because there's also a FadeTransition from the MaterialPageRoute, which is up the tree
6342 );
6343 return fadeTransitionWidget.opacity.value;
6344}
6345
6346Material _railMaterial(WidgetTester tester) {
6347 // The first material is for the rail, and the rest are for the destinations.
6348 return tester.firstWidget<Material>(
6349 find.descendant(of: find.byType(NavigationRail), matching: find.byType(Material)),
6350 );
6351}
6352
6353Widget _buildWidget(Widget child, {bool useMaterial3 = true, bool isRTL = false}) {
6354 return MaterialApp(
6355 theme: ThemeData(useMaterial3: useMaterial3),
6356 home: Directionality(
6357 textDirection: isRTL ? TextDirection.rtl : TextDirection.ltr,
6358 child: Scaffold(
6359 body: Row(
6360 children: <Widget>[
6361 child,
6362 const Expanded(child: Text('body')),
6363 ],
6364 ),
6365 ),
6366 ),
6367 );
6368}
6369
6370ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
6371 return tester
6372 .firstWidget<Container>(
6373 find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)),
6374 )
6375 .decoration
6376 as ShapeDecoration?;
6377}
6378