bottom_app_bar_test.dart 29.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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 7
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
8
library;
9

10 11
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
12
import 'package:flutter_test/flutter_test.dart';
13
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
14
void main() {
15
  testWidgetsWithLeakTracking('Material3 - Shadow effect is not doubled', (WidgetTester tester) async {
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    // Regression test for https://github.com/flutter/flutter/issues/123064
    debugDisableShadows = false;

    const double elevation = 1;
    const Color shadowColor = Colors.black;

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.light(useMaterial3: true),
        home: const Scaffold(
          bottomNavigationBar: BottomAppBar(
            elevation: elevation,
            shadowColor: shadowColor,
          ),
        ),
      ),
    );

    final Finder finder = find.byType(BottomAppBar);
    expect(finder, paints..shadow(color: shadowColor, elevation: elevation));
    expect(finder, paintsExactlyCountTimes(#drawShadow, 1));

    debugDisableShadows = true;
  });

41
  testWidgetsWithLeakTracking('Material3 - Only one layer with `color` is painted', (WidgetTester tester) async {
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
    // Regression test for https://github.com/flutter/flutter/issues/122667
    const Color bottomAppBarColor = Colors.black45;

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.light(useMaterial3: true),
        home: const Scaffold(
          bottomNavigationBar: BottomAppBar(
            color: bottomAppBarColor,
            // Avoid getting a surface tint color, to keep the color check below simple
            elevation: 0,
          ),
        ),
      ),
    );

    // There should be just one color layer, and with the specified color.
    final Finder finder = find.descendant(
      of: find.byType(BottomAppBar),
      matching: find.byWidgetPredicate((Widget widget) {
        // A color layer is probably a [PhysicalShape] or [PhysicalModel],
        // either used directly or backing a [Material] (one without
        // [MaterialType.transparency]).
        return widget is PhysicalShape || widget is PhysicalModel;
      }),
    );
    final Widget widget = tester.widgetList(finder).single;
    if (widget is PhysicalShape) {
      expect(widget.color, bottomAppBarColor);
    } else if (widget is PhysicalModel) {
      expect(widget.color, bottomAppBarColor);
    } else {
      // Should be unreachable: compare with the finder.
      assert(false);
    }
  });

79
  testWidgetsWithLeakTracking('No overlap with floating action button', (WidgetTester tester) async {
80
    await tester.pumpWidget(
81 82
      const MaterialApp(
        home: Scaffold(
83
          floatingActionButton: FloatingActionButton(
84 85
            onPressed: null,
          ),
86 87 88
          bottomNavigationBar: ShapeListener(
            BottomAppBar(
              child: SizedBox(height: 100.0),
89
            ),
90
          ),
91 92 93 94 95 96
        ),
      ),
    );

    final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener));
    final RenderBox renderBox = tester.renderObject(find.byType(BottomAppBar));
97
    final Path expectedPath = Path()
98 99 100 101 102 103 104 105
      ..addRect(Offset.zero & renderBox.size);

    final Path actualPath = shapeListenerState.cache.value;
    expect(
      actualPath,
      coversSameAreaAs(
        expectedPath,
        areaToCompare: (Offset.zero & renderBox.size).inflate(5.0),
106
      ),
107 108
    );
  });
109

110
  testWidgetsWithLeakTracking('Material2 - Custom shape', (WidgetTester tester) async {
111 112 113 114 115 116 117 118 119
    final Key key = UniqueKey();
    Future<void> pump(FloatingActionButtonLocation location) async {
      await tester.pumpWidget(
        SizedBox(
          width: 200,
          height: 200,
          child: RepaintBoundary(
            key: key,
            child: MaterialApp(
120
              theme: ThemeData(useMaterial3: false),
121 122 123 124 125
              home: Scaffold(
                floatingActionButton: FloatingActionButton(
                  onPressed: () { },
                ),
                floatingActionButtonLocation: location,
126
                bottomNavigationBar: const BottomAppBar(
127
                  shape: AutomaticNotchedShape(
128 129
                    BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
                    ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))),
130 131 132
                  ),
                  notchMargin: 10.0,
                  color: Colors.green,
133
                  child: SizedBox(height: 100.0),
134 135 136 137 138 139 140 141 142 143
                ),
              ),
            ),
          ),
        ),
      );
    }
    await pump(FloatingActionButtonLocation.endDocked);
    await expectLater(
      find.byKey(key),
144
      matchesGoldenFile('m2_bottom_app_bar.custom_shape.1.png'),
145 146 147 148 149
    );
    await pump(FloatingActionButtonLocation.centerDocked);
    await tester.pumpAndSettle();
    await expectLater(
      find.byKey(key),
150
      matchesGoldenFile('m2_bottom_app_bar.custom_shape.2.png'),
151
    );
152
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572
153

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  testWidgetsWithLeakTracking('Material3 - Custom shape', (WidgetTester tester) async {
    final Key key = UniqueKey();
    Future<void> pump(FloatingActionButtonLocation location) async {
      await tester.pumpWidget(
        SizedBox(
          width: 200,
          height: 200,
          child: RepaintBoundary(
            key: key,
            child: MaterialApp(
              theme: ThemeData(useMaterial3: true),
              home: Scaffold(
                floatingActionButton: FloatingActionButton(
                  onPressed: () { },
                ),
                floatingActionButtonLocation: location,
                bottomNavigationBar: const BottomAppBar(
                  shape: AutomaticNotchedShape(
                    BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
                    ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))),
174
                  ),
175 176 177
                  notchMargin: 10.0,
                  color: Colors.green,
                  child: SizedBox(height: 100.0),
178 179
                ),
              ),
180 181
            ),
          ),
182
        ),
183 184 185 186 187 188
      );
    }
    await pump(FloatingActionButtonLocation.endDocked);
    await expectLater(
      find.byKey(key),
      matchesGoldenFile('m3_bottom_app_bar.custom_shape.1.png'),
189
    );
190 191 192 193 194 195 196
    await pump(FloatingActionButtonLocation.centerDocked);
    await tester.pumpAndSettle();
    await expectLater(
      find.byKey(key),
      matchesGoldenFile('m3_bottom_app_bar.custom_shape.2.png'),
    );
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572
197

198
  testWidgetsWithLeakTracking('Custom Padding', (WidgetTester tester) async {
199
    const EdgeInsets customPadding = EdgeInsets.all(10);
200 201
    await tester.pumpWidget(
      MaterialApp(
202
        theme: ThemeData.from(colorScheme: const ColorScheme.light()),
203 204 205 206 207 208 209
        home: Builder(
          builder: (BuildContext context) {
            return const Scaffold(
              body: Align(
                alignment: Alignment.bottomCenter,
                child: BottomAppBar(
                  padding: customPadding,
210 211 212 213
                  child: ColoredBox(
                    color: Colors.green,
                    child: SizedBox(width: 300, height: 60),
                  ),
214 215 216 217 218 219 220 221 222 223
                ),
              ),
            );
          },
        ),
      ),
    );

    final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar));
    expect(bottomAppBar.padding, customPadding);
224 225
    final Rect babRect = tester.getRect(find.byType(BottomAppBar));
    final Rect childRect = tester.getRect(find.byType(ColoredBox));
226 227 228 229
    expect(childRect, const Rect.fromLTRB(250, 530, 550, 590));
    expect(babRect, const Rect.fromLTRB(240, 520, 560, 600));
  });

230
  testWidgetsWithLeakTracking('Material2 - Color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async {
231
    await tester.pumpWidget(
232
      MaterialApp(
233
        theme: ThemeData(useMaterial3: false),
234
        home: Builder(
235
          builder: (BuildContext context) {
236
            return Theme(
237
              data: Theme.of(context).copyWith(bottomAppBarColor: const Color(0xffffff00)),
238
              child: const Scaffold(
239
                floatingActionButton: FloatingActionButton(
240 241
                  onPressed: null,
                ),
242
                bottomNavigationBar: BottomAppBar(),
243 244
              ),
            );
245
          },
246 247 248 249 250 251 252 253 254 255
        ),
      ),
    );

    final PhysicalShape physicalShape =
      tester.widget(find.byType(PhysicalShape).at(0));

    expect(physicalShape.color, const Color(0xffffff00));
  });

256
  testWidgetsWithLeakTracking('Material2 - Color overrides theme color', (WidgetTester tester) async {
257
    await tester.pumpWidget(
258
      MaterialApp(
259
        theme: ThemeData(useMaterial3: false),
260
        home: Builder(
261
          builder: (BuildContext context) {
262
            return Theme(
263
              data: Theme.of(context).copyWith(bottomAppBarColor: const Color(0xffffff00)),
264
              child: const Scaffold(
265
                floatingActionButton: FloatingActionButton(
266 267
                  onPressed: null,
                ),
268
                bottomNavigationBar: BottomAppBar(
269
                  color: Color(0xff0000ff),
270 271 272
                ),
              ),
            );
273
          },
274 275 276 277 278 279
        ),
      ),
    );

    final PhysicalShape physicalShape =
      tester.widget(find.byType(PhysicalShape).at(0));
280
    final Material material = tester.widget(find.byType(Material).at(1));
281 282

    expect(physicalShape.color, const Color(0xff0000ff));
283 284 285 286
    expect(material.color, null); /* no value in Material 2. */
  });


287
  testWidgetsWithLeakTracking('Material3 - Color overrides theme color', (WidgetTester tester) async {
288 289 290
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.light(useMaterial3: true).copyWith(
291 292
          bottomAppBarColor: const Color(0xffffff00),
        ),
293 294 295 296 297 298 299 300
        home: Builder(
          builder: (BuildContext context) {
            return const Scaffold(
                floatingActionButton: FloatingActionButton(
                  onPressed: null,
                ),
                bottomNavigationBar: BottomAppBar(
                  color: Color(0xff0000ff),
301
                  surfaceTintColor: Colors.transparent,
302 303 304 305 306 307 308
                ),
            );
          },
        ),
      ),
    );

309 310
    final PhysicalShape physicalShape = tester.widget(
        find.descendant(of: find.byType(BottomAppBar), matching: find.byType(PhysicalShape)));
311 312

    expect(physicalShape.color, const Color(0xff0000ff));
313 314
  });

315
  testWidgetsWithLeakTracking('Material3 - Shadow color is transparent', (WidgetTester tester) async {
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: true,
        ),
        home: const Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: null,
          ),
          bottomNavigationBar: BottomAppBar(
            color: Color(0xff0000ff),
          ),
        ),
      )
    );

331 332
    final PhysicalShape physicalShape = tester.widget(
        find.descendant(of: find.byType(BottomAppBar), matching: find.byType(PhysicalShape)));
333

334
    expect(physicalShape.shadowColor, Colors.transparent);
335 336
  });

337
  testWidgetsWithLeakTracking('Material2 - Dark theme applies an elevation overlay color', (WidgetTester tester) async {
338 339
    await tester.pumpWidget(
      MaterialApp(
340
        theme: ThemeData.from(useMaterial3: false, colorScheme: const ColorScheme.dark()),
341 342 343 344 345
        home: Scaffold(
          bottomNavigationBar: BottomAppBar(
            color: const ColorScheme.dark().surface,
          ),
        ),
346
      ),
347 348 349 350 351 352 353 354
    );

    final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0));

    // For the default dark theme the overlay color for elevation 8 is 0xFF2D2D2D
    expect(physicalShape.color, const Color(0xFF2D2D2D));
  });

355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  testWidgetsWithLeakTracking('Material3 - Dark theme applies an elevation overlay color', (WidgetTester tester) async {
    const ColorScheme colorScheme = ColorScheme.dark();
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.from(useMaterial3: true, colorScheme: colorScheme),
        home: Scaffold(
          bottomNavigationBar: BottomAppBar(
            color: colorScheme.surface,
          ),
        ),
      ),
    );

    final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0));

    const double elevation = 3.0; // Default for M3.
    final Color overlayColor = ElevationOverlay.applySurfaceTint(colorScheme.surface, colorScheme.surfaceTint, elevation);
    expect(physicalShape.color, overlayColor);
  });

375 376
  // This is a regression test for a bug we had where toggling the notch on/off
  // would crash, as the shouldReclip method of ShapeBorderClipper or
377
  // _BottomAppBarClipper would try an illegal downcast.
378
  testWidgetsWithLeakTracking('toggle shape to null', (WidgetTester tester) async {
379
    await tester.pumpWidget(
380 381
      const MaterialApp(
        home: Scaffold(
382 383
          bottomNavigationBar: BottomAppBar(
            shape: RectangularNotch(),
384 385 386 387 388 389
          ),
        ),
      ),
    );

    await tester.pumpWidget(
390 391
      const MaterialApp(
        home: Scaffold(
392
          bottomNavigationBar: BottomAppBar(),
393 394 395 396 397
        ),
      ),
    );

    await tester.pumpWidget(
398 399
      const MaterialApp(
        home: Scaffold(
400 401
          bottomNavigationBar: BottomAppBar(
            shape: RectangularNotch(),
402 403 404 405 406
          ),
        ),
      ),
    );
  });
407

408
  testWidgetsWithLeakTracking('no notch when notch param is null', (WidgetTester tester) async {
409
    await tester.pumpWidget(
410 411
      const MaterialApp(
        home: Scaffold(
412
          bottomNavigationBar: ShapeListener(BottomAppBar()),
413
          floatingActionButton: FloatingActionButton(
414
            onPressed: null,
415
            child: Icon(Icons.add),
416 417 418 419 420 421 422 423
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        ),
      ),
    );

    final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener));
    final RenderBox renderBox = tester.renderObject(find.byType(BottomAppBar));
424
    final Path expectedPath = Path()
425 426 427 428 429 430 431 432 433
      ..addRect(Offset.zero & renderBox.size);

    final Path actualPath = shapeListenerState.cache.value;

    expect(
      actualPath,
      coversSameAreaAs(
        expectedPath,
        areaToCompare: (Offset.zero & renderBox.size).inflate(5.0),
434
      ),
435 436 437
    );
  });

438
  testWidgetsWithLeakTracking('notch no margin', (WidgetTester tester) async {
439
    await tester.pumpWidget(
440 441
      const MaterialApp(
        home: Scaffold(
442 443 444
          bottomNavigationBar: ShapeListener(
            BottomAppBar(
              shape: RectangularNotch(),
445
              notchMargin: 0.0,
446
              child: SizedBox(height: 100.0),
447
            ),
448
          ),
449
          floatingActionButton: FloatingActionButton(
450
            onPressed: null,
451
            child: Icon(Icons.add),
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        ),
      ),
    );

    final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener));
    final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar));
    final Size babSize = babBox.size;
    final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton));
    final Size fabSize = fabBox.size;

    final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0);
    final double fabRight = fabLeft + fabSize.width;
    final double fabBottom = fabSize.height / 2.0;

468
    final Path expectedPath = Path()
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
      ..moveTo(0.0, 0.0)
      ..lineTo(fabLeft, 0.0)
      ..lineTo(fabLeft, fabBottom)
      ..lineTo(fabRight, fabBottom)
      ..lineTo(fabRight, 0.0)
      ..lineTo(babSize.width, 0.0)
      ..lineTo(babSize.width, babSize.height)
      ..lineTo(0.0, babSize.height)
      ..close();

    final Path actualPath = shapeListenerState.cache.value;

    expect(
      actualPath,
      coversSameAreaAs(
        expectedPath,
        areaToCompare: (Offset.zero & babSize).inflate(5.0),
486
      ),
487 488 489
    );
  });

490
  testWidgetsWithLeakTracking('notch with margin', (WidgetTester tester) async {
491
    await tester.pumpWidget(
492 493
      const MaterialApp(
        home: Scaffold(
494 495 496
          bottomNavigationBar: ShapeListener(
            BottomAppBar(
              shape: RectangularNotch(),
497
              notchMargin: 6.0,
498
              child: SizedBox(height: 100.0),
499
            ),
500
          ),
501
          floatingActionButton: FloatingActionButton(
502
            onPressed: null,
503
            child: Icon(Icons.add),
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        ),
      ),
    );

    final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener));
    final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar));
    final Size babSize = babBox.size;
    final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton));
    final Size fabSize = fabBox.size;

    final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0) - 6.0;
    final double fabRight = fabLeft + fabSize.width + 6.0;
    final double fabBottom = 6.0 + fabSize.height / 2.0;

520
    final Path expectedPath = Path()
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
      ..moveTo(0.0, 0.0)
      ..lineTo(fabLeft, 0.0)
      ..lineTo(fabLeft, fabBottom)
      ..lineTo(fabRight, fabBottom)
      ..lineTo(fabRight, 0.0)
      ..lineTo(babSize.width, 0.0)
      ..lineTo(babSize.width, babSize.height)
      ..lineTo(0.0, babSize.height)
      ..close();

    final Path actualPath = shapeListenerState.cache.value;

    expect(
      actualPath,
      coversSameAreaAs(
        expectedPath,
        areaToCompare: (Offset.zero & babSize).inflate(5.0),
538
      ),
539 540
    );
  });
541

542
  testWidgetsWithLeakTracking('Material2 - Observes safe area', (WidgetTester tester) async {
543
    await tester.pumpWidget(
544 545 546
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const MediaQuery(
547 548
          data: MediaQueryData(
            padding: EdgeInsets.all(50.0),
549
          ),
550 551 552 553
          child: Scaffold(
            bottomNavigationBar: BottomAppBar(
              child: Center(
                child: Text('safe'),
554 555 556 557 558 559 560 561 562 563 564 565
              ),
            ),
          ),
        ),
      ),
    );

    expect(
      tester.getBottomLeft(find.widgetWithText(Center, 'safe')),
      const Offset(50.0, 550.0),
    );
  });
566

567 568 569 570 571 572 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
  testWidgetsWithLeakTracking('Material3 - Observes safe area', (WidgetTester tester) async {
    const double safeAreaPadding = 50.0;
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: const MediaQuery(
          data: MediaQueryData(
            padding: EdgeInsets.all(safeAreaPadding),
          ),
          child: Scaffold(
            bottomNavigationBar: BottomAppBar(
              child: Center(
                child: Text('safe'),
              ),
            ),
          ),
        ),
      ),
    );

    const double appBarVerticalPadding = 12.0;
    const double appBarHorizontalPadding = 16.0;
    expect(
      tester.getBottomLeft(find.widgetWithText(Center, 'safe')),
      const Offset(
        safeAreaPadding + appBarHorizontalPadding,
        600 - safeAreaPadding - appBarVerticalPadding,
      ),
    );
  });

598
  testWidgetsWithLeakTracking('clipBehavior is propagated', (WidgetTester tester) async {
599
    await tester.pumpWidget(
600 601
      const MaterialApp(
        home: Scaffold(
602 603 604 605 606
          bottomNavigationBar: BottomAppBar(
            shape: RectangularNotch(),
            notchMargin: 0.0,
            child: SizedBox(height: 100.0),
          ),
607 608 609 610 611 612 613 614
        ),
      ),
    );

    PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape));
    expect(physicalShape.clipBehavior, Clip.none);

    await tester.pumpWidget(
615 616
      const MaterialApp(
        home: Scaffold(
617 618 619 620 621
          bottomNavigationBar:
          BottomAppBar(
            shape: RectangularNotch(),
            notchMargin: 0.0,
            clipBehavior: Clip.antiAliasWithSaveLayer,
622
            child: SizedBox(height: 100.0),
623 624 625 626 627 628 629 630
          ),
        ),
      ),
    );

    physicalShape = tester.widget(find.byType(PhysicalShape));
    expect(physicalShape.clipBehavior, Clip.antiAliasWithSaveLayer);
  });
631

632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668
  testWidgetsWithLeakTracking('Material2 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/80878
    final ThemeData theme = ThemeData(useMaterial3: false);
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Scaffold(
          floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
          floatingActionButton: FloatingActionButton(
            backgroundColor: Colors.green,
            child: const Icon(Icons.home),
            onPressed: () {},
          ),
          body: Stack(
            children: <Widget>[
              Container(
                color: Colors.amber,
              ),
              Container(
                alignment: Alignment.bottomCenter,
                child: BottomAppBar(
                  color: Colors.green,
                  shape: const CircularNotchedRectangle(),
                  child: Container(height: 50),
                ),
              ),
            ],
          ),
        ),
      ),
    );

    expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584));
    expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 50));
  });

  testWidgetsWithLeakTracking('Material3 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async {
669
    // Regression test for https://github.com/flutter/flutter/issues/80878
670
    final ThemeData theme = ThemeData(useMaterial3: true);
671 672
    await tester.pumpWidget(
      MaterialApp(
673
        theme: theme,
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
        home: Scaffold(
          floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
          floatingActionButton: FloatingActionButton(
            backgroundColor: Colors.green,
            child: const Icon(Icons.home),
            onPressed: () {},
          ),
          body: Stack(
            children: <Widget>[
              Container(
                color: Colors.amber,
              ),
              Container(
                alignment: Alignment.bottomCenter,
                child: BottomAppBar(
                  color: Colors.green,
                  shape: const CircularNotchedRectangle(),
                  child: Container(height: 50),
                ),
              ),
            ],
          ),
        ),
      ),
    );

    expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584));
701
    expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 80));
702
  });
703

704
  testWidgetsWithLeakTracking('notch with margin and top padding, home safe area', (WidgetTester tester) async {
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
    // Regression test for https://github.com/flutter/flutter/issues/90024
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(
          padding: EdgeInsets.only(top: 128),
        ),
        child: MaterialApp(
          useInheritedMediaQuery: true,
          home: SafeArea(
            child: Scaffold(
              bottomNavigationBar: ShapeListener(
                BottomAppBar(
                  shape: RectangularNotch(),
                  notchMargin: 6.0,
                  child: SizedBox(height: 100.0),
                ),
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: null,
                child: Icon(Icons.add),
              ),
              floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
            ),
          ),
        ),
      ),
    );

    final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener));
    final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar));
    final Size babSize = babBox.size;
    final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton));
    final Size fabSize = fabBox.size;

    final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0) - 6.0;
    final double fabRight = fabLeft + fabSize.width + 6.0;
    final double fabBottom = 6.0 + fabSize.height / 2.0;

    final Path expectedPath = Path()
      ..moveTo(0.0, 0.0)
      ..lineTo(fabLeft, 0.0)
      ..lineTo(fabLeft, fabBottom)
      ..lineTo(fabRight, fabBottom)
      ..lineTo(fabRight, 0.0)
      ..lineTo(babSize.width, 0.0)
      ..lineTo(babSize.width, babSize.height)
      ..lineTo(0.0, babSize.height)
      ..close();

    final Path actualPath = shapeListenerState.cache.value;

    expect(
      actualPath,
      coversSameAreaAs(
        expectedPath,
        areaToCompare: (Offset.zero & babSize).inflate(5.0),
      ),
    );
  });
764

765
  testWidgetsWithLeakTracking('BottomAppBar does not apply custom clipper without FAB', (WidgetTester tester) async {
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
    Widget buildWidget({Widget? fab}) {
      return MaterialApp(
        home: Scaffold(
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
          floatingActionButton: fab,
          bottomNavigationBar: BottomAppBar(
            color: Colors.green,
            shape: const CircularNotchedRectangle(),
            child: Container(height: 50),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildWidget(fab: FloatingActionButton(onPressed: () { })));

    PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0));
    expect(physicalShape.clipper.toString(), '_BottomAppBarClipper');

    await tester.pumpWidget(buildWidget());

    physicalShape = tester.widget(find.byType(PhysicalShape).at(0));
    expect(physicalShape.clipper.toString(), 'ShapeBorderClipper');
  });
789

790
  testWidgetsWithLeakTracking('Material3 - BottomAppBar adds bottom padding to height', (WidgetTester tester) async {
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
    const double bottomPadding = 35.0;

    await tester.pumpWidget(
      MediaQuery(
        data: const MediaQueryData(
          padding: EdgeInsets.only(bottom: bottomPadding),
          viewPadding: EdgeInsets.only(bottom: bottomPadding),
        ),
        child: MaterialApp(
          theme: ThemeData(useMaterial3: true),
          home: Scaffold(
            floatingActionButtonLocation: FloatingActionButtonLocation.endContained,
            floatingActionButton: FloatingActionButton(onPressed: () { }),
            bottomNavigationBar: BottomAppBar(
              child: IconButton(
                icon: const Icon(Icons.search),
                onPressed: () {},
              ),
            ),
          ),
        ),
      )
    );

    final Rect bottomAppBar = tester.getRect(find.byType(BottomAppBar));
    final Rect iconButton = tester.getRect(find.widgetWithIcon(IconButton, Icons.search));
    final Rect fab = tester.getRect(find.byType(FloatingActionButton));

    // The height of the bottom app bar should be its height(default is 80.0) + bottom safe area height.
    expect(bottomAppBar.height, 80.0 + bottomPadding);

    // The vertical position of the icon button and fab should be center of the area excluding the bottom padding.
    final double barCenter = bottomAppBar.topLeft.dy + (bottomAppBar.height - bottomPadding) / 2;
    expect(iconButton.center.dy, barCenter);
    expect(fab.center.dy, barCenter);
  });
827 828 829 830
}

// The bottom app bar clip path computation is only available at paint time.
// In order to examine the notch path we implement this caching painter which
831
// at paint time looks for a descendant PhysicalShape and caches the
832 833 834 835
// clip path it is using.
class ClipCachePainter extends CustomPainter {
  ClipCachePainter(this.context);

836
  late Path value;
837 838 839 840
  BuildContext context;

  @override
  void paint(Canvas canvas, Size size) {
841 842
    final RenderPhysicalShape physicalShape = findPhysicalShapeChild(context)!;
    value = physicalShape.clipper!.getClip(size);
843 844
  }

845 846
  RenderPhysicalShape? findPhysicalShapeChild(BuildContext context) {
    RenderPhysicalShape? result;
847
    context.visitChildElements((Element e) {
848
      final RenderObject renderObject = e.findRenderObject()!;
849 850
      if (renderObject.runtimeType == RenderPhysicalShape) {
        assert(result == null);
851
        result = renderObject as RenderPhysicalShape;
852 853 854 855 856 857 858 859 860 861 862 863 864 865
      } else {
        result = findPhysicalShapeChild(e);
      }
    });
    return result;
  }

  @override
  bool shouldRepaint(ClipCachePainter oldDelegate) {
    return true;
  }
}

class ShapeListener extends StatefulWidget {
866
  const ShapeListener(this.child, { super.key });
867 868 869 870

  final Widget child;

  @override
871
  State createState() => ShapeListenerState();
872 873 874 875 876 877

}

class ShapeListenerState extends State<ShapeListener> {
  @override
  Widget build(BuildContext context) {
878
    return CustomPaint(
879
      painter: cache,
880
      child: widget.child,
881 882 883
    );
  }

884
  late ClipCachePainter cache;
885 886 887 888

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
889
    cache = ClipCachePainter(context);
890 891 892
  }

}
893

894
class RectangularNotch extends NotchedShape {
895 896 897
  const RectangularNotch();

  @override
898
  Path getOuterPath(Rect host, Rect? guest) {
899
    if (guest == null) {
900
      return Path()..addRect(host);
901
    }
902
    return Path()
903 904 905 906 907 908 909 910 911 912 913
      ..moveTo(host.left, host.top)
      ..lineTo(guest.left, host.top)
      ..lineTo(guest.left, guest.bottom)
      ..lineTo(guest.right, guest.bottom)
      ..lineTo(guest.right, host.top)
      ..lineTo(host.right, host.top)
      ..lineTo(host.right, host.bottom)
      ..lineTo(host.left, host.bottom)
      ..close();
  }
}