bottom_tab_bar_test.dart 19.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
xster's avatar
xster committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:typed_data';

xster's avatar
xster committed
7
import 'package:flutter/cupertino.dart';
8 9
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
10
import 'package:flutter/rendering.dart';
xster's avatar
xster committed
11 12
import 'package:flutter_test/flutter_test.dart';

13
import '../image_data.dart';
14
import '../widgets/semantics_tester.dart';
xster's avatar
xster committed
15

16
Future<void> pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async {
17
  await tester.pumpWidget(
18 19
    Localizations(
      locale: const Locale('en', 'US'),
20
      delegates: const <LocalizationsDelegate<dynamic>>[
21 22 23 24 25 26 27
        DefaultWidgetsLocalizations.delegate,
        DefaultCupertinoLocalizations.delegate,
      ],
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: widget,
      ),
28 29 30 31
    ),
  );
}

32 33
Future<void> main() async {

xster's avatar
xster committed
34
  testWidgets('Need at least 2 tabs', (WidgetTester tester) async {
35 36
    await expectLater(
      () => pumpWidgetWithBoilerplate(tester, CupertinoTabBar(
37
        items: <BottomNavigationBarItem>[
38
          BottomNavigationBarItem(
39
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
40
            label: 'Tab 1',
xster's avatar
xster committed
41 42
          ),
        ],
43
      )),
44
      throwsA(isAssertionError.having(
45 46 47 48 49
        (AssertionError error) => error.toString(),
        '.toString()',
        contains('items.length'),
      )),
    );
xster's avatar
xster committed
50 51 52
  });

  testWidgets('Active and inactive colors', (WidgetTester tester) async {
53
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
54
      data: const MediaQueryData(),
55
      child: CupertinoTabBar(
56
        items: <BottomNavigationBarItem>[
57
          BottomNavigationBarItem(
58
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
59
            label: 'Tab 1',
60
          ),
61
          BottomNavigationBarItem(
62
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
63
            label: 'Tab 2',
64 65 66 67 68 69
          ),
        ],
        currentIndex: 1,
        activeColor: const Color(0xFF123456),
        inactiveColor: const Color(0xFF654321),
      ),
xster's avatar
xster committed
70 71 72 73 74 75
    ));

    final RichText actualInactive = tester.widget(find.descendant(
      of: find.text('Tab 1'),
      matching: find.byType(RichText),
    ));
76
    expect(actualInactive.text.style!.color, const Color(0xFF654321));
xster's avatar
xster committed
77 78 79 80 81

    final RichText actualActive = tester.widget(find.descendant(
      of: find.text('Tab 2'),
      matching: find.byType(RichText),
    ));
82
    expect(actualActive.text.style!.color, const Color(0xFF123456));
xster's avatar
xster committed
83 84
  });

85 86 87 88 89

  testWidgets('BottomNavigationBar.label will create a text widget', (WidgetTester tester) async {
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(),
      child: CupertinoTabBar(
90
        items: <BottomNavigationBarItem>[
91
          BottomNavigationBarItem(
92
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
93 94 95
            label: 'Tab 1',
          ),
          BottomNavigationBarItem(
96
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
97 98 99 100 101 102 103 104 105 106 107
            label: 'Tab 2',
          ),
        ],
        currentIndex: 1,
      ),
    ));

    expect(find.text('Tab 1'), findsOneWidget);
    expect(find.text('Tab 2'), findsOneWidget);
  });

108 109 110 111 112 113 114 115 116 117 118 119 120 121
  testWidgets('Active and inactive colors dark mode', (WidgetTester tester) async {
    const CupertinoDynamicColor dynamicActiveColor = CupertinoDynamicColor.withBrightness(
      color: Color(0xFF000000),
      darkColor: Color(0xFF000001),
    );

    const CupertinoDynamicColor dynamicInactiveColor = CupertinoDynamicColor.withBrightness(
      color: Color(0xFF000002),
      darkColor: Color(0xFF000003),
    );

    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(),
      child: CupertinoTabBar(
122
        items: <BottomNavigationBarItem>[
123
          BottomNavigationBarItem(
124
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
125
            label: 'Tab 1',
126 127
          ),
          BottomNavigationBarItem(
128
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
129
            label: 'Tab 2',
130 131 132 133 134 135 136 137 138 139 140 141
          ),
        ],
        currentIndex: 1,
        activeColor: dynamicActiveColor,
        inactiveColor: dynamicInactiveColor,
      ),
    ));

    RichText actualInactive = tester.widget(find.descendant(
      of: find.text('Tab 1'),
      matching: find.byType(RichText),
    ));
142
    expect(actualInactive.text.style!.color!.value, 0xFF000002);
143 144 145 146 147

    RichText actualActive = tester.widget(find.descendant(
      of: find.text('Tab 2'),
      matching: find.byType(RichText),
    ));
148
    expect(actualActive.text.style!.color!.value, 0xFF000000);
149 150 151 152 153 154 155

    final RenderDecoratedBox renderDecoratedBox = tester.renderObject(find.descendant(
      of: find.byType(BackdropFilter),
      matching: find.byType(DecoratedBox),
    ));

    // Border color is resolved correctly.
156
    final BoxDecoration decoration1 = renderDecoratedBox.decoration as BoxDecoration;
157
    expect(decoration1.border!.top.color.value, 0x4C000000);
158 159 160 161 162

    // Switch to dark mode.
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
        data: const MediaQueryData(platformBrightness: Brightness.dark),
        child: CupertinoTabBar(
163
          items: <BottomNavigationBarItem>[
164
            BottomNavigationBarItem(
165
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
166
              label: 'Tab 1',
167 168
            ),
            BottomNavigationBarItem(
169
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
170
              label: 'Tab 2',
171 172 173 174 175 176 177 178 179 180 181 182
            ),
          ],
          currentIndex: 1,
          activeColor: dynamicActiveColor,
          inactiveColor: dynamicInactiveColor,
        ),
    ));

    actualInactive = tester.widget(find.descendant(
        of: find.text('Tab 1'),
        matching: find.byType(RichText),
    ));
183
    expect(actualInactive.text.style!.color!.value, 0xFF000003);
184 185 186 187 188

    actualActive = tester.widget(find.descendant(
        of: find.text('Tab 2'),
        matching: find.byType(RichText),
    ));
189
    expect(actualActive.text.style!.color!.value, 0xFF000001);
190 191

    // Border color is resolved correctly.
192
    final BoxDecoration decoration2 = renderDecoratedBox.decoration as BoxDecoration;
193
    expect(decoration2.border!.top.color.value, 0x29000000);
194 195
  });

xster's avatar
xster committed
196 197 198 199
  testWidgets('Tabs respects themes', (WidgetTester tester) async {
    await tester.pumpWidget(
      CupertinoApp(
        home: CupertinoTabBar(
200
          items: <BottomNavigationBarItem>[
xster's avatar
xster committed
201
            BottomNavigationBarItem(
202
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
203
              label: 'Tab 1',
xster's avatar
xster committed
204 205
            ),
            BottomNavigationBarItem(
206
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
207
              label: 'Tab 2',
xster's avatar
xster committed
208 209 210 211 212 213 214 215 216 217 218
            ),
          ],
          currentIndex: 1,
        ),
      ),
    );

    RichText actualInactive = tester.widget(find.descendant(
      of: find.text('Tab 1'),
      matching: find.byType(RichText),
    ));
219
    expect(actualInactive.text.style!.color!.value, 0xFF999999);
xster's avatar
xster committed
220 221 222 223 224

    RichText actualActive = tester.widget(find.descendant(
      of: find.text('Tab 2'),
      matching: find.byType(RichText),
    ));
225
    expect(actualActive.text.style!.color, CupertinoColors.activeBlue);
xster's avatar
xster committed
226 227 228 229 230

    await tester.pumpWidget(
      CupertinoApp(
        theme: const CupertinoThemeData(brightness: Brightness.dark),
        home: CupertinoTabBar(
231
          items: <BottomNavigationBarItem>[
xster's avatar
xster committed
232
            BottomNavigationBarItem(
233
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
234
              label: 'Tab 1',
xster's avatar
xster committed
235 236
            ),
            BottomNavigationBarItem(
237
              icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
238
              label: 'Tab 2',
xster's avatar
xster committed
239 240 241 242 243 244 245 246 247 248 249
            ),
          ],
          currentIndex: 1,
        ),
      ),
    );

    actualInactive = tester.widget(find.descendant(
      of: find.text('Tab 1'),
      matching: find.byType(RichText),
    ));
250
    expect(actualInactive.text.style!.color!.value, 0xFF757575);
xster's avatar
xster committed
251 252 253 254 255 256

    actualActive = tester.widget(find.descendant(
      of: find.text('Tab 2'),
      matching: find.byType(RichText),
    ));

257
    expect(actualActive.text.style!.color, isSameColorAs(CupertinoColors.activeBlue.darkColor));
xster's avatar
xster committed
258 259
  });

260
  testWidgets('Use active icon', (WidgetTester tester) async {
261
    final MemoryImage activeIcon = MemoryImage(Uint8List.fromList(kBlueSquarePng));
262
    final MemoryImage inactiveIcon = MemoryImage(Uint8List.fromList(kTransparentImage));
263 264 265 266

    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(),
      child: CupertinoTabBar(
267
        items: <BottomNavigationBarItem>[
268
          BottomNavigationBarItem(
269
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
270
            label: 'Tab 1',
271 272 273 274
          ),
          BottomNavigationBarItem(
            icon: ImageIcon(inactiveIcon),
            activeIcon: ImageIcon(activeIcon),
275
            label: 'Tab 2',
276 277 278 279 280 281 282 283 284 285
          ),
        ],
        currentIndex: 1,
        activeColor: const Color(0xFF123456),
        inactiveColor: const Color(0xFF654321),
      ),
    ));

    final Image image = tester.widget(find.descendant(
      of: find.widgetWithText(GestureDetector, 'Tab 2'),
286
      matching: find.byType(Image),
287 288 289 290 291 292
    ));

    expect(image.color, const Color(0xFF123456));
    expect(image.image, activeIcon);
  });

293
  testWidgets('Adjusts height to account for bottom padding', (WidgetTester tester) async {
294
    final CupertinoTabBar tabBar = CupertinoTabBar(
295
      items: <BottomNavigationBarItem>[
296
        BottomNavigationBarItem(
297
          icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
298
          label: 'Aka',
xster's avatar
xster committed
299
        ),
300
        BottomNavigationBarItem(
301
          icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
302
          label: 'Shiro',
xster's avatar
xster committed
303 304
        ),
      ],
305 306 307
    );

    // Verify height with no bottom padding.
308
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
309
      data: const MediaQueryData(),
310
      child: CupertinoTabScaffold(
311 312 313 314 315 316 317 318 319
        tabBar: tabBar,
        tabBuilder: (BuildContext context, int index) {
          return const Placeholder();
        },
      ),
    ));
    expect(tester.getSize(find.byType(CupertinoTabBar)).height, 50.0);

    // Verify height with bottom padding.
320
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
321
      data: const MediaQueryData(padding: EdgeInsets.only(bottom: 40.0)),
322
      child: CupertinoTabScaffold(
323 324 325 326 327 328 329 330 331
        tabBar: tabBar,
        tabBuilder: (BuildContext context, int index) {
          return const Placeholder();
        },
      ),
    ));
    expect(tester.getSize(find.byType(CupertinoTabBar)).height, 90.0);
  });

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  testWidgets('Set custom height', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/51704
    const double tabBarHeight = 56.0;
    final CupertinoTabBar tabBar = CupertinoTabBar(
      height: tabBarHeight,
      items: <BottomNavigationBarItem>[
        BottomNavigationBarItem(
          icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
          label: 'Aka',
        ),
        BottomNavigationBarItem(
          icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
          label: 'Shiro',
        ),
      ],
    );

    // Verify height with no bottom padding.
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(),
      child: CupertinoTabScaffold(
        tabBar: tabBar,
        tabBuilder: (BuildContext context, int index) {
          return const Placeholder();
        },
      ),
    ));
    expect(tester.getSize(find.byType(CupertinoTabBar)).height, tabBarHeight);

    // Verify height with bottom padding.
    const double bottomPadding = 40.0;
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
      child: CupertinoTabScaffold(
        tabBar: tabBar,
        tabBuilder: (BuildContext context, int index) {
          return const Placeholder();
        },
      ),
    ));
    expect(tester.getSize(find.byType(CupertinoTabBar)).height, tabBarHeight + bottomPadding);
  });

375
  testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async {
376
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
377
      data: const MediaQueryData(),
378
      child: CupertinoTabBar(
379
        items: <BottomNavigationBarItem>[
380
          BottomNavigationBarItem(
381
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
382
            label: 'Tab 1',
383
          ),
384
          BottomNavigationBarItem(
385
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
386
            label: 'Tab 2',
387 388 389
          ),
        ],
      ),
xster's avatar
xster committed
390 391 392 393
    ));

    expect(find.byType(BackdropFilter), findsOneWidget);

394
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
395
      data: const MediaQueryData(),
396
      child: CupertinoTabBar(
397
        items: <BottomNavigationBarItem>[
398
          BottomNavigationBarItem(
399
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
400
            label: 'Tab 1',
401
          ),
402
          BottomNavigationBarItem(
403
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
404
            label: 'Tab 2',
405 406 407 408
          ),
        ],
        backgroundColor: const Color(0xFFFFFFFF), // Opaque white.
      ),
xster's avatar
xster committed
409 410 411 412 413 414
    ));

    expect(find.byType(BackdropFilter), findsNothing);
  });

  testWidgets('Tap callback', (WidgetTester tester) async {
415
    late int callbackTab;
xster's avatar
xster committed
416

417 418 419
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
      data: const MediaQueryData(),
      child: CupertinoTabBar(
420
        items: <BottomNavigationBarItem>[
421
          BottomNavigationBarItem(
422
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
423
            label: 'Tab 1',
424 425
          ),
          BottomNavigationBarItem(
426
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
427
            label: 'Tab 2',
428 429 430 431 432
          ),
        ],
        currentIndex: 1,
        onTap: (int tab) { callbackTab = tab; },
      ),
xster's avatar
xster committed
433 434 435 436
    ));

    await tester.tap(find.text('Tab 1'));
    expect(callbackTab, 0);
437 438 439

    await tester.tap(find.text('Tab 2'));
    expect(callbackTab, 1);
xster's avatar
xster committed
440
  });
441 442

  testWidgets('tabs announce semantics', (WidgetTester tester) async {
443
    final SemanticsTester semantics = SemanticsTester(tester);
444

445
    await pumpWidgetWithBoilerplate(tester, MediaQuery(
446
      data: const MediaQueryData(),
447
      child: CupertinoTabBar(
448
        items: <BottomNavigationBarItem>[
449
          BottomNavigationBarItem(
450
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
451
            label: 'Tab 1',
452
          ),
453
          BottomNavigationBarItem(
454
            icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
455
            label: 'Tab 2',
456 457 458 459 460 461 462
          ),
        ],
      ),
    ));

    expect(semantics, includesNodeWith(
      label: 'Tab 1',
463
      hint: 'Tab 1 of 2',
464 465 466 467 468 469
      flags: <SemanticsFlag>[SemanticsFlag.isSelected],
      textDirection: TextDirection.ltr,
    ));

    expect(semantics, includesNodeWith(
      label: 'Tab 2',
470
      hint: 'Tab 2 of 2',
471 472 473 474 475
      textDirection: TextDirection.ltr,
    ));

    semantics.dispose();
  });
476

477
  testWidgets('Label of items should be nullable', (WidgetTester tester) async {
478
    final MemoryImage iconProvider = MemoryImage(Uint8List.fromList(kTransparentImage));
479 480 481
    final List<int> itemsTapped = <int>[];

    await pumpWidgetWithBoilerplate(
482 483 484 485 486 487 488 489
      tester,
      MediaQuery(
        data: const MediaQueryData(),
        child: CupertinoTabBar(
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: ImageIcon(
                MemoryImage(Uint8List.fromList(kTransparentImage)),
490
              ),
491 492 493 494 495
              label: 'Tab 1',
            ),
            BottomNavigationBarItem(
              icon: ImageIcon(
                iconProvider,
496
              ),
497 498 499 500 501 502
            ),
          ],
          onTap: (int index) => itemsTapped.add(index),
        ),
      ),
    );
503 504 505 506

    expect(find.text('Tab 1'), findsOneWidget);

    final Finder finder = find.byWidgetPredicate(
507 508
      (Widget widget) => widget is Image && widget.image == iconProvider,
    );
509 510 511 512 513

    await tester.tap(finder);
    expect(itemsTapped, <int>[1]);
  });

514
  testWidgets('Hide border hides the top border of the tabBar', (WidgetTester tester) async {
515
    await pumpWidgetWithBoilerplate(
516 517 518 519 520 521 522 523
      tester,
      MediaQuery(
        data: const MediaQueryData(),
        child: CupertinoTabBar(
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: ImageIcon(
                MemoryImage(Uint8List.fromList(kTransparentImage)),
524
              ),
525 526 527 528 529
              label: 'Tab 1',
            ),
            BottomNavigationBarItem(
              icon: ImageIcon(
                MemoryImage(Uint8List.fromList(kTransparentImage)),
530
              ),
531 532 533 534 535 536
              label: 'Tab 2',
            ),
          ],
        ),
      ),
    );
537 538

    final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox));
539
    final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
540 541 542
    expect(boxDecoration.border, isNotNull);

    await pumpWidgetWithBoilerplate(
543 544 545 546 547 548 549 550
      tester,
      MediaQuery(
        data: const MediaQueryData(),
        child: CupertinoTabBar(
          items: <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: ImageIcon(
                MemoryImage(Uint8List.fromList(kTransparentImage)),
551
              ),
552 553 554 555 556
              label: 'Tab 1',
            ),
            BottomNavigationBarItem(
              icon: ImageIcon(
                MemoryImage(Uint8List.fromList(kTransparentImage)),
557
              ),
558 559 560 561 562 563 564 565
              label: 'Tab 2',
            ),
          ],
          backgroundColor: const Color(0xFFFFFFFF), // Opaque white.
          border: null,
        ),
      ),
    );
566 567 568 569

    final DecoratedBox decoratedBoxHiddenBorder =
        tester.widget(find.byType(DecoratedBox));
    final BoxDecoration boxDecorationHiddenBorder =
570
        decoratedBoxHiddenBorder.decoration as BoxDecoration;
571 572
    expect(boxDecorationHiddenBorder.border, isNull);
  });
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

  testWidgets('Hovering over tab bar item updates cursor to clickable on Web', (WidgetTester tester) async {
    await pumpWidgetWithBoilerplate(
      tester,
      MediaQuery(
        data: const MediaQueryData(),
        child: Center(
          child: CupertinoTabBar(
            items: const <BottomNavigationBarItem>[
              BottomNavigationBarItem(
                icon: Icon(CupertinoIcons.alarm),
                label: 'Tab 1',
              ),
              BottomNavigationBarItem(
                icon: Icon(CupertinoIcons.app_badge),
                label: 'Tab 2',
              ),
            ],
          ),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: const Offset(10, 10));
    await tester.pumpAndSettle();
    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);

    final Offset tabItem = tester.getCenter(find.text('Tab 1'));
    await gesture.moveTo(tabItem);
    addTearDown(gesture.removePointer);
    await tester.pumpAndSettle();
    expect(
      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
      kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
    );
  });
610
}