// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show DisplayFeature, DisplayFeatureState, DisplayFeatureType, SemanticsFlag;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';

void main() {
  testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async {
    final Key targetKey = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
            return const Text('Next');
          },
        },
        home: Material(
          child: Center(
            child: Builder(
              key: targetKey,
              builder: (BuildContext context) {
                return PopupMenuButton<int>(
                  onSelected: (int value) {
                    Navigator.pushNamed(context, '/next');
                  },
                  itemBuilder: (BuildContext context) {
                    return <PopupMenuItem<int>>[
                      const PopupMenuItem<int>(
                        value: 1,
                        child: Text('One'),
                      ),
                    ];
                  },
                );
              },
            ),
          ),
        ),
      ),
    );

    await tester.tap(find.byKey(targetKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(find.text('One'), findsOneWidget);
    expect(find.text('Next'), findsNothing);

    await tester.tap(find.text('One'));
    await tester.pump(); // return the future
    await tester.pump(); // start the navigation
    await tester.pump(const Duration(seconds: 1)); // end the navigation

    expect(find.text('One'), findsNothing);
    expect(find.text('Next'), findsOneWidget);
  });

  testWidgets('PopupMenuButton calls onOpened callback when the menu is opened', (WidgetTester tester) async {
    int opens = 0;
    late BuildContext popupContext;
    final Key noItemsKey = UniqueKey();
    final Key noCallbackKey = UniqueKey();
    final Key withCallbackKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: noItemsKey,
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[];
                },
                onOpened: () => opens++,
              ),
              PopupMenuButton<int>(
                key: noCallbackKey,
                itemBuilder: (BuildContext context) {
                  popupContext = context;
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
              ),
              PopupMenuButton<int>(
                key: withCallbackKey,
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me, too!'),
                    ),
                  ];
                },
                onOpened: () => opens++,
              ),
            ],
          ),
        ),
      ),
    );

    // Make sure callback is not called when the menu is not shown
    await tester.tap(find.byKey(noItemsKey));
    await tester.pump();
    expect(opens, equals(0));

    // Make sure everything works if no callback is provided
    await tester.tap(find.byKey(noCallbackKey));
    await tester.pump();
    expect(opens, equals(0));

    // Close the opened menu
    Navigator.of(popupContext).pop();
    await tester.pump();

    // Make sure callback is called when the button is tapped
    await tester.tap(find.byKey(withCallbackKey));
    await tester.pump();
    expect(opens, equals(1));
  });

  testWidgets('PopupMenuButton calls onCanceled callback when an item is not selected', (WidgetTester tester) async {
    int cancels = 0;
    late BuildContext popupContext;
    final Key noCallbackKey = UniqueKey();
    final Key withCallbackKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: noCallbackKey,
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
              ),
              PopupMenuButton<int>(
                key: withCallbackKey,
                onCanceled: () => cancels++,
                itemBuilder: (BuildContext context) {
                  popupContext = context;
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me, too!'),
                    ),
                  ];
                },
              ),
            ],
          ),
        ),
      ),
    );

    // Make sure everything works if no callback is provided
    await tester.tap(find.byKey(noCallbackKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.tapAt(Offset.zero);
    await tester.pump();
    expect(cancels, equals(0));

    // Make sure callback is called when a non-selection tap occurs
    await tester.tap(find.byKey(withCallbackKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    await tester.tapAt(Offset.zero);
    await tester.pump();
    expect(cancels, equals(1));

    // Make sure callback is called when back navigation occurs
    await tester.tap(find.byKey(withCallbackKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    Navigator.of(popupContext).pop();
    await tester.pump();
    expect(cancels, equals(2));
  });

  testWidgets('disabled PopupMenuButton will not call itemBuilder, onOpened, onSelected or onCanceled', (WidgetTester tester) async {
    final GlobalKey popupButtonKey = GlobalKey();
    bool itemBuilderCalled = false;
    bool onOpenedCalled = false;
    bool onSelectedCalled = false;
    bool onCanceledCalled = false;

    Widget buildApp({bool directional = false}) {
      return MaterialApp(
        home: Builder(builder: (BuildContext context) {
          return MediaQuery(
            data: MediaQuery.of(context).copyWith(
              navigationMode: NavigationMode.directional,
            ),
            child: Material(
              child: Column(
                children: <Widget>[
                  PopupMenuButton<int>(
                    enabled: false,
                    child: Text('Tap Me', key: popupButtonKey),
                    itemBuilder: (BuildContext context) {
                      itemBuilderCalled = true;
                      return <PopupMenuEntry<int>>[
                        const PopupMenuItem<int>(
                          value: 1,
                          child: Text('Tap me please!'),
                        ),
                      ];
                    },
                    onOpened: ()=> onOpenedCalled = true,
                    onSelected: (int selected) => onSelectedCalled = true,
                    onCanceled: () => onCanceledCalled = true,
                  ),
                ],
              ),
            ),
          );
        }),
      );
    }

    await tester.pumpWidget(buildApp());

    // Try to bring up the popup menu and select the first item from it
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    expect(itemBuilderCalled, isFalse);
    expect(onOpenedCalled, isFalse);
    expect(onSelectedCalled, isFalse);

    // Try to bring up the popup menu and tap outside it to cancel the menu
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    await tester.tapAt(Offset.zero);
    await tester.pumpAndSettle();
    expect(itemBuilderCalled, isFalse);
    expect(onOpenedCalled, isFalse);
    expect(onCanceledCalled, isFalse);

    // Test again, with directional navigation mode and after focusing the button.
    await tester.pumpWidget(buildApp(directional: true));

    // Try to bring up the popup menu and select the first item from it
    Focus.of(popupButtonKey.currentContext!).requestFocus();
    await tester.pumpAndSettle();
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    expect(itemBuilderCalled, isFalse);
    expect(onOpenedCalled, isFalse);
    expect(onSelectedCalled, isFalse);

    // Try to bring up the popup menu and tap outside it to cancel the menu
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    await tester.tapAt(Offset.zero);
    await tester.pumpAndSettle();
    expect(itemBuilderCalled, isFalse);
    expect(onOpenedCalled, isFalse);
    expect(onCanceledCalled, isFalse);
  });

  testWidgets('disabled PopupMenuButton is not focusable', (WidgetTester tester) async {
    final Key popupButtonKey = UniqueKey();
    final GlobalKey childKey = GlobalKey();
    bool itemBuilderCalled = false;
    bool onOpenedCalled = false;
    bool onSelectedCalled = false;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: popupButtonKey,
                enabled: false,
                child: Container(key: childKey),
                itemBuilder: (BuildContext context) {
                  itemBuilderCalled = true;
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                onOpened: () => onOpenedCalled = true,
                onSelected: (int selected) => onSelectedCalled = true,
              ),
            ],
          ),
        ),
      ),
    );
    Focus.of(childKey.currentContext!).requestFocus();
    await tester.pump();

    expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse);
    expect(itemBuilderCalled, isFalse);
    expect(onOpenedCalled, isFalse);
    expect(onSelectedCalled, isFalse);
  });

  testWidgets('disabled PopupMenuButton is focusable with directional navigation', (WidgetTester tester) async {
    final Key popupButtonKey = UniqueKey();
    final GlobalKey childKey = GlobalKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Builder(builder: (BuildContext context) {
          return MediaQuery(
            data: MediaQuery.of(context).copyWith(
              navigationMode: NavigationMode.directional,
            ),
            child: Material(
              child: Column(
                children: <Widget>[
                  PopupMenuButton<int>(
                    key: popupButtonKey,
                    enabled: false,
                    child: Container(key: childKey),
                    itemBuilder: (BuildContext context) {
                      return <PopupMenuEntry<int>>[
                        const PopupMenuItem<int>(
                          value: 1,
                          child: Text('Tap me please!'),
                        ),
                      ];
                    },
                    onSelected: (int selected) {},
                  ),
                ],
              ),
            ),
          );
        }),
      ),
    );
    Focus.of(childKey.currentContext!).requestFocus();
    await tester.pump();

    expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isTrue);
  });

  testWidgets('PopupMenuItem onTap callback is called when defined', (WidgetTester tester) async {
    final List<int> menuItemTapCounters = <int>[0, 0];

    await tester.pumpWidget(
      TestApp(
        textDirection: TextDirection.ltr,
        child: Material(
          child: RepaintBoundary(
            child: PopupMenuButton<void>(
              child: const Text('Actions'),
              itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[
                PopupMenuItem<void>(
                  child: const Text('First option'),
                  onTap: () {
                    menuItemTapCounters[0] += 1;
                  },
                ),
                PopupMenuItem<void>(
                  child: const Text('Second option'),
                  onTap: () {
                    menuItemTapCounters[1] += 1;
                  },
                ),
                const PopupMenuItem<void>(
                  child: Text('Option without onTap'),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    // Tap the first time
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('First option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[1, 0]);

    // Tap the item again
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('First option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 0]);

    // Tap a different item
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Second option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 1]);

    // Tap an item without onTap
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Option without onTap'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 1]);
  });

  testWidgets('PopupMenuItem can have both onTap and value', (WidgetTester tester) async {
    final List<int> menuItemTapCounters = <int>[0, 0];
    String? selected;

    await tester.pumpWidget(
      TestApp(
        textDirection: TextDirection.ltr,
        child: Material(
          child: RepaintBoundary(
            child: PopupMenuButton<String>(
              child: const Text('Actions'),
              onSelected: (String value) { selected = value; },
              itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
                PopupMenuItem<String>(
                  value: 'first',
                  child: const Text('First option'),
                  onTap: () {
                    menuItemTapCounters[0] += 1;
                  },
                ),
                PopupMenuItem<String>(
                  value: 'second',
                  child: const Text('Second option'),
                  onTap: () {
                    menuItemTapCounters[1] += 1;
                  },
                ),
               const PopupMenuItem<String>(
                 value: 'third',
                 child: Text('Option without onTap'),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    // Tap the first item
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('First option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[1, 0]);
    expect(selected, 'first');

    // Tap the item again
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('First option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 0]);
    expect(selected, 'first');

    // Tap a different item
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Second option'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 1]);
    expect(selected, 'second');

    // Tap an item without onTap
    await tester.tap(find.text('Actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Option without onTap'));
    await tester.pumpAndSettle();
    expect(menuItemTapCounters, <int>[2, 1]);
    expect(selected, 'third');
  });

  testWidgets('PopupMenuItem is only focusable when enabled', (WidgetTester tester) async {
    final Key popupButtonKey = UniqueKey();
    final GlobalKey childKey = GlobalKey();
    bool itemBuilderCalled = false;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: popupButtonKey,
                itemBuilder: (BuildContext context) {
                  itemBuilderCalled = true;
                  return <PopupMenuEntry<int>>[
                    PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!', key: childKey),
                    ),
                  ];
                },
              ),
            ],
          ),
        ),
      ),
    );

    // Open the popup to build and show the menu contents.
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();
    final FocusNode childNode = Focus.of(childKey.currentContext!);
    // Now that the contents are shown, request focus on the child text.
    childNode.requestFocus();
    await tester.pumpAndSettle();
    expect(itemBuilderCalled, isTrue);

    // Make sure that the focus went where we expected it to.
    expect(childNode.hasPrimaryFocus, isTrue);
    itemBuilderCalled = false;

    // Close the popup.
    await tester.tap(find.byKey(popupButtonKey), warnIfMissed: false);
    await tester.pumpAndSettle();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: popupButtonKey,
                itemBuilder: (BuildContext context) {
                  itemBuilderCalled = true;
                  return <PopupMenuEntry<int>>[
                    PopupMenuItem<int>(
                      enabled: false,
                      value: 1,
                      child: Text('Tap me please!', key: childKey),
                    ),
                  ];
                },
              ),
            ],
          ),
        ),
      ),
    );
    await tester.pumpAndSettle();
    // Open the popup again to rebuild the contents with enabled == false.
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();

    expect(itemBuilderCalled, isTrue);
    expect(Focus.of(childKey.currentContext!).hasPrimaryFocus, isFalse);
  });

  testWidgets('PopupMenuButton is horizontal on iOS', (WidgetTester tester) async {
    Widget build(TargetPlatform platform) {
      debugDefaultTargetPlatformOverride = platform;
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            actions: <Widget>[
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuItem<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('One'),
                    ),
                  ];
                },
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(build(TargetPlatform.android));

    expect(find.byIcon(Icons.more_vert), findsOneWidget);
    expect(find.byIcon(Icons.more_horiz), findsNothing);

    await tester.pumpWidget(build(TargetPlatform.iOS));
    await tester.pumpAndSettle(); // Run theme change animation.

    expect(find.byIcon(Icons.more_vert), findsNothing);
    expect(find.byIcon(Icons.more_horiz), findsOneWidget);

    await tester.pumpWidget(build(TargetPlatform.macOS));
    await tester.pumpAndSettle(); // Run theme change animation.

    expect(find.byIcon(Icons.more_vert), findsNothing);
    expect(find.byIcon(Icons.more_horiz), findsOneWidget);

    debugDefaultTargetPlatformOverride = null;
  });

  group('PopupMenuButton with Icon', () {
    // Helper function to create simple and valid popup menus.
    List<PopupMenuItem<int>> simplePopupMenuItemBuilder(BuildContext context) {
      return <PopupMenuItem<int>>[
        const PopupMenuItem<int>(
            value: 1,
            child: Text('1'),
        ),
      ];
    }

    testWidgets('PopupMenuButton fails when given both child and icon', (WidgetTester tester) async {
      expect(() {
        PopupMenuButton<int>(
            icon: const Icon(Icons.view_carousel),
            itemBuilder: simplePopupMenuItemBuilder,
            child: const Text('heyo'),
        );
      }, throwsAssertionError);
    });

    testWidgets('PopupMenuButton creates IconButton when given an icon', (WidgetTester tester) async {
      final PopupMenuButton<int> button = PopupMenuButton<int>(
        icon: const Icon(Icons.view_carousel),
        itemBuilder: simplePopupMenuItemBuilder,
      );

      await tester.pumpWidget(MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              actions: <Widget>[button],
            ),
          ),
        ),
      );

      expect(find.byType(IconButton), findsOneWidget);
      expect(find.byIcon(Icons.view_carousel), findsOneWidget);
    });
  });

  testWidgets('PopupMenu positioning', (WidgetTester tester) async {
    final Widget testButton = PopupMenuButton<int>(
      itemBuilder: (BuildContext context) {
        return <PopupMenuItem<int>>[
          const PopupMenuItem<int>(value: 1, child: Text('AAA')),
          const PopupMenuItem<int>(value: 2, child: Text('BBB')),
          const PopupMenuItem<int>(value: 3, child: Text('CCC')),
        ];
      },
      child: const SizedBox(
        height: 100.0,
        width: 100.0,
        child: Text('XXX'),
      ),
    );
    bool popupMenu(Widget widget) {
      final String widgetType = widget.runtimeType.toString();
      // TODO(mraleph): Remove the old case below.
      return widgetType == '_PopupMenu<int?>' // normal case
          || widgetType == '_PopupMenu'; // for old versions of Dart that don't reify method type arguments
    }

    Future<void> openMenu(TextDirection textDirection, Alignment alignment) async {
      return TestAsyncUtils.guard<void>(() async {
        await tester.pumpWidget(Container()); // reset in case we had a menu up already
        await tester.pumpWidget(TestApp(
          textDirection: textDirection,
          child: Align(
            alignment: alignment,
            child: testButton,
          ),
        ));
        await tester.tap(find.text('XXX'));
        await tester.pump();
      });
    }

    Future<void> testPositioningDown(
      WidgetTester tester,
      TextDirection textDirection,
      Alignment alignment,
      TextDirection growthDirection,
      Rect startRect,
    ) {
      return TestAsyncUtils.guard<void>(() async {
        await openMenu(textDirection, alignment);
        Rect rect = tester.getRect(find.byWidgetPredicate(popupMenu));
        expect(rect, startRect);
        bool doneVertically = false;
        bool doneHorizontally = false;
        do {
          await tester.pump(const Duration(milliseconds: 20));
          final Rect newRect = tester.getRect(find.byWidgetPredicate(popupMenu));
          expect(newRect.top, rect.top);
          if (doneVertically) {
            expect(newRect.bottom, rect.bottom);
          } else {
            if (newRect.bottom == rect.bottom) {
              doneVertically = true;
            } else {
              expect(newRect.bottom, greaterThan(rect.bottom));
            }
          }
          switch (growthDirection) {
            case TextDirection.rtl:
              expect(newRect.right, rect.right);
              if (doneHorizontally) {
                expect(newRect.left, rect.left);
              } else {
                if (newRect.left == rect.left) {
                  doneHorizontally = true;
                } else {
                  expect(newRect.left, lessThan(rect.left));
                }
              }
              break;
            case TextDirection.ltr:
              expect(newRect.left, rect.left);
              if (doneHorizontally) {
                expect(newRect.right, rect.right);
              } else {
                if (newRect.right == rect.right) {
                  doneHorizontally = true;
                } else {
                  expect(newRect.right, greaterThan(rect.right));
                }
              }
              break;
          }
          rect = newRect;
        } while (tester.binding.hasScheduledFrame);
      });
    }

    Future<void> testPositioningDownThenUp(
      WidgetTester tester,
      TextDirection textDirection,
      Alignment alignment,
      TextDirection growthDirection,
      Rect startRect,
    ) {
      return TestAsyncUtils.guard<void>(() async {
        await openMenu(textDirection, alignment);
        Rect rect = tester.getRect(find.byWidgetPredicate(popupMenu));
        expect(rect, startRect);
        int verticalStage = 0; // 0=down, 1=up, 2=done
        bool doneHorizontally = false;
        do {
          await tester.pump(const Duration(milliseconds: 20));
          final Rect newRect = tester.getRect(find.byWidgetPredicate(popupMenu));
          switch (verticalStage) {
            case 0:
              if (newRect.top < rect.top) {
                verticalStage = 1;
                expect(newRect.bottom, greaterThanOrEqualTo(rect.bottom));
                break;
              }
              expect(newRect.top, rect.top);
              expect(newRect.bottom, greaterThan(rect.bottom));
              break;
            case 1:
              if (newRect.top == rect.top) {
                verticalStage = 2;
                expect(newRect.bottom, rect.bottom);
                break;
              }
              expect(newRect.top, lessThan(rect.top));
              expect(newRect.bottom, rect.bottom);
              break;
            case 2:
              expect(newRect.bottom, rect.bottom);
              expect(newRect.top, rect.top);
              break;
            default:
              assert(false);
          }
          switch (growthDirection) {
            case TextDirection.rtl:
              expect(newRect.right, rect.right);
              if (doneHorizontally) {
                expect(newRect.left, rect.left);
              } else {
                if (newRect.left == rect.left) {
                  doneHorizontally = true;
                } else {
                  expect(newRect.left, lessThan(rect.left));
                }
              }
              break;
            case TextDirection.ltr:
              expect(newRect.left, rect.left);
              if (doneHorizontally) {
                expect(newRect.right, rect.right);
              } else {
                if (newRect.right == rect.right) {
                  doneHorizontally = true;
                } else {
                  expect(newRect.right, greaterThan(rect.right));
                }
              }
              break;
          }
          rect = newRect;
        } while (tester.binding.hasScheduledFrame);
      });
    }

    await testPositioningDown(tester, TextDirection.ltr, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.ltr, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.ltr, Alignment.topCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.topCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 8.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.ltr, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.ltr, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.ltr, Alignment.center, TextDirection.ltr, const Rect.fromLTWH(350.0, 250.0, 0.0, 0.0));
    await testPositioningDown(tester, TextDirection.rtl, Alignment.center, TextDirection.rtl, const Rect.fromLTWH(450.0, 250.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 500.0, 0.0, 0.0));
    await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 500.0, 0.0, 0.0));
  });

  testWidgets('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async {
    final Key buttonKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Example')),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Overlay(
              initialEntries: <OverlayEntry>[
                OverlayEntry(
                  builder: (_) => Center(
                    child: PopupMenuButton<int>(
                      key: buttonKey,
                      itemBuilder: (_) => <PopupMenuItem<int>>[
                        const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
                        const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
                      ],
                      child: const Text('Show Menu'),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    final Finder buttonFinder = find.byKey(buttonKey);
    final Finder popupFinder = find.bySemanticsLabel('Popup menu');
    await tester.tap(buttonFinder);
    await tester.pumpAndSettle();

    final Offset buttonTopLeft = tester.getTopLeft(buttonFinder);
    expect(tester.getTopLeft(popupFinder), buttonTopLeft);
  });

  testWidgets('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async {
    final Key buttonKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Example')),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<dynamic>(
                  builder: (BuildContext context) {
                    return Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Center(
                        child: PopupMenuButton<int>(
                          key: buttonKey,
                          itemBuilder: (_) => <PopupMenuItem<int>>[
                            const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
                            const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
                          ],
                          child: const Text('Show Menu'),
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ),
      ),
    );

    final Finder buttonFinder = find.byKey(buttonKey);
    final Finder popupFinder = find.bySemanticsLabel('Popup menu');
    await tester.tap(buttonFinder);
    await tester.pumpAndSettle();

    final Offset buttonTopLeft = tester.getTopLeft(buttonFinder);
    expect(tester.getTopLeft(popupFinder), buttonTopLeft);
  });

  testWidgets('PopupMenu positioning around display features', (WidgetTester tester) async {
    final Key buttonKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(
            size: Size(800, 600),
            displayFeatures: <DisplayFeature>[
              // A 20-pixel wide vertical display feature, similar to a foldable
              // with a visible hinge. Splits the display into two "virtual screens"
              // and the popup menu should never overlap the display feature.
              DisplayFeature(
                bounds: Rect.fromLTRB(390, 0, 410, 600),
                type: DisplayFeatureType.cutout,
                state: DisplayFeatureState.unknown,
              ),
            ],
          ),
          child: Scaffold(
            body: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<dynamic>(
                  builder: (BuildContext context) {
                    return Padding(
                      // Position the button in the top-right of the first "virtual screen"
                      padding: const EdgeInsets.only(right:390.0),
                      child: Align(
                        alignment: Alignment.topRight,
                        child: PopupMenuButton<int>(
                          key: buttonKey,
                          itemBuilder: (_) => <PopupMenuItem<int>>[
                            const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
                            const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
                          ],
                          child: const Text('Show Menu'),
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ),
      ),
    );

    final Finder buttonFinder = find.byKey(buttonKey);
    final Finder popupFinder = find.bySemanticsLabel('Popup menu');
    await tester.tap(buttonFinder);
    await tester.pumpAndSettle();

    // Since the display feature splits the display into 2 sub-screens, popup
    // menu should be positioned to fit in the first virtual screen, where the
    // originating button is.
    // The 8 pixels is [_kMenuScreenPadding].
    expect(tester.getTopRight(popupFinder), const Offset(390 - 8, 8));
  });

  testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async {
    late BuildContext popupContext;

    await tester.pumpWidget(MaterialApp(
      home: MediaQuery(
        data: const MediaQueryData(
          padding: EdgeInsets.all(50.0),
        ),
        child: Material(
          child: PopupMenuButton<int>(
            itemBuilder: (BuildContext context) {
              popupContext = context;
              return <PopupMenuItem<int>>[
                PopupMenuItem<int>(
                  value: 1,
                  child: Builder(
                    builder: (BuildContext context) {
                      popupContext = context;
                      return const Text('AAA');
                    },
                  ),
                ),
              ];
            },
            child: const SizedBox(
              height: 100.0,
              width: 100.0,
              child: Text('XXX'),
            ),
          ),
        ),
      ),
    ));

    await tester.tap(find.text('XXX'));

    await tester.pump();

    expect(MediaQuery.of(popupContext).padding, EdgeInsets.zero);
  });

  testWidgets('Popup Menu Offset Test', (WidgetTester tester) async {
    PopupMenuButton<int> buildMenuButton({Offset offset = Offset.zero}) {
      return PopupMenuButton<int>(
        offset: offset,
        itemBuilder: (BuildContext context) {
          return <PopupMenuItem<int>>[
            PopupMenuItem<int>(
              value: 1,
              child: Builder(
                builder: (BuildContext context) {
                  return const Text('AAA');
                },
              ),
            ),
          ];
        },
      );
    }

    // Popup a menu without any offset.
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: buildMenuButton(),
          ),
        ),
      ),
    );

    // Popup the menu.
    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    // Initial state, the menu start at Offset(8.0, 8.0), the 8 pixels is edge padding when offset.dx < 8.0.
    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));

    // Collapse the menu.
    await tester.tap(find.byType(IconButton), warnIfMissed: false);
    await tester.pumpAndSettle();

    // Popup a new menu with Offset(50.0, 50.0).
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: buildMenuButton(offset: const Offset(50.0, 50.0)),
          ),
        ),
      ),
    );

    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    // This time the menu should start at Offset(50.0, 50.0), the padding only added when offset.dx < 8.0.
    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(50.0, 50.0));
  });

  testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: PopupMenuButton<int>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<int>>[
                const PopupMenuItem<int>(value: 1, child: Text('1')),
                const PopupMenuItem<int>(value: 2, child: Text('2')),
                const PopupMenuItem<int>(value: 3, child: Text('3')),
                const PopupMenuItem<int>(value: 4, child: Text('4')),
                const PopupMenuItem<int>(value: 5, child: Text('5')),
              ];
            },
            child: const SizedBox(
              height: 100.0,
              width: 100.0,
              child: Text('XXX'),
            ),
          ),
        ),
      ),
    );
    await tester.tap(find.text('XXX'));
    await tester.pumpAndSettle();

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[
                      SemanticsFlag.scopesRoute,
                      SemanticsFlag.namesRoute,
                    ],
                    label: 'Popup menu',
                    textDirection: TextDirection.ltr,
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[
                          SemanticsFlag.hasImplicitScrolling,
                        ],
                        children: <TestSemantics>[
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '1',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '2',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '3',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '4',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '5',
                            textDirection: TextDirection.ltr,
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
              TestSemantics(
                  actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
                  label: 'Dismiss',
                  textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreId: true, ignoreTransform: true, ignoreRect: true,
    ));

    semantics.dispose();
  });

  testWidgets('PopupMenuItem merges the semantics of its descendants', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: PopupMenuButton<int>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<int>>[
                PopupMenuItem<int>(
                  value: 1,
                  child: Row(
                    children: <Widget>[
                      Semantics(
                        child: const Text('test1'),
                      ),
                      Semantics(
                        child: const Text('test2'),
                      ),
                    ],
                  ),
                ),
              ];
            },
            child: const SizedBox(
              height: 100.0,
              width: 100.0,
              child: Text('XXX'),
            ),
          ),
        ),
      ),
    );
    await tester.tap(find.text('XXX'));
    await tester.pumpAndSettle();

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[
                      SemanticsFlag.scopesRoute,
                      SemanticsFlag.namesRoute,
                    ],
                    label: 'Popup menu',
                    textDirection: TextDirection.ltr,
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[
                          SemanticsFlag.hasImplicitScrolling,
                        ],
                        children: <TestSemantics>[
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: 'test1\ntest2',
                            textDirection: TextDirection.ltr,
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
              TestSemantics(
                  actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
                  label: 'Dismiss',
                  textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreId: true, ignoreTransform: true, ignoreRect: true,
    ));

    semantics.dispose();
  });

  testWidgets('disabled PopupMenuItem has correct semantics', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/45044.
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: PopupMenuButton<int>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<int>>[
                const PopupMenuItem<int>(value: 1, child: Text('1')),
                const PopupMenuItem<int>(value: 2, enabled: false ,child: Text('2')),
                const PopupMenuItem<int>(value: 3, child: Text('3')),
                const PopupMenuItem<int>(value: 4, child: Text('4')),
                const PopupMenuItem<int>(value: 5, child: Text('5')),
              ];
            },
            child: const SizedBox(
              height: 100.0,
              width: 100.0,
              child: Text('XXX'),
            ),
          ),
        ),
      ),
    );
    await tester.tap(find.text('XXX'));
    await tester.pumpAndSettle();

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[
                      SemanticsFlag.scopesRoute,
                      SemanticsFlag.namesRoute,
                    ],
                    label: 'Popup menu',
                    textDirection: TextDirection.ltr,
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[
                          SemanticsFlag.hasImplicitScrolling,
                        ],
                        children: <TestSemantics>[
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '1',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                            ],
                            actions: <SemanticsAction>[],
                            label: '2',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '3',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '4',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isButton,
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: '5',
                            textDirection: TextDirection.ltr,
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
              TestSemantics(
                  actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
                  label: 'Dismiss',
                  textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreId: true, ignoreTransform: true, ignoreRect: true,
    ));

    semantics.dispose();
  });

  testWidgets('PopupMenuButton PopupMenuDivider', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/27072

    late String selectedValue;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              onSelected: (String result) {
                selectedValue = result;
              },
              initialValue: '1',
              child: const Text('Menu Button'),
              itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
                const PopupMenuItem<String>(
                  value: '1',
                  child: Text('1'),
                ),
                const PopupMenuDivider(),
                const PopupMenuItem<String>(
                  value: '2',
                  child: Text('2'),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    await tester.tap(find.text('Menu Button'));
    await tester.pumpAndSettle();
    expect(find.text('1'), findsOneWidget);
    expect(find.byType(PopupMenuDivider), findsOneWidget);
    expect(find.text('2'), findsOneWidget);

    await tester.tap(find.text('1'));
    await tester.pumpAndSettle();
    expect(selectedValue, '1');

    await tester.tap(find.text('Menu Button'));
    await tester.pumpAndSettle();
    expect(find.text('1'), findsOneWidget);
    expect(find.byType(PopupMenuDivider), findsOneWidget);
    expect(find.text('2'), findsOneWidget);

    await tester.tap(find.text('2'));
    await tester.pumpAndSettle();
    expect(selectedValue, '2');
  });

  testWidgets('PopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupMenuButtonKey,
              child: const Text('button'),
              onSelected: (String result) { },
              itemBuilder: (BuildContext context) {
                return <PopupMenuEntry<String>>[
                  // This menu item's height will be 48 because the default minimum height
                  // is 48 and the height of the text is less than 48.
                  const PopupMenuItem<String>(
                    value: '0',
                    child: Text('Item 0'),
                  ),
                  // This menu item's height parameter specifies its minimum height. The
                  // overall height of the menu item will be 50 because the child's
                  // height 40, is less than 50.
                  const PopupMenuItem<String>(
                    height: 50,
                    value: '1',
                    child: SizedBox(
                      height: 40,
                      child: Text('Item 1'),
                    ),
                  ),
                  // This menu item's height parameter specifies its minimum height, so the
                  // overall height of the menu item will be 75.
                  const PopupMenuItem<String>(
                    height: 75,
                    value: '2',
                    child: SizedBox(
                      child: Text('Item 2'),
                    ),
                  ),
                  // This menu item's height will be 100.
                  const PopupMenuItem<String>(
                    value: '3',
                    child: SizedBox(
                      height: 100,
                      child: Text('Item 3'),
                    ),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Show the menu
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    // The menu items and their InkWells should have the expected vertical size
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 50);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 75);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 0')).height, 48);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 1')).height, 50);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 2')).height, 75);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 3')).height, 100);

    // Menu item children which whose height is less than the PopupMenuItem
    // are vertically centered.
    expect(
      tester.getRect(find.widgetWithText(menuItemType, 'Item 0')).center.dy,
      tester.getRect(find.text('Item 0')).center.dy,
    );
    expect(
      tester.getRect(find.widgetWithText(menuItemType, 'Item 2')).center.dy,
      tester.getRect(find.text('Item 2')).center.dy,
    );
  });

  testWidgets('PopupMenuItem custom padding', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupMenuButtonKey,
              child: const Text('button'),
              onSelected: (String result) { },
              itemBuilder: (BuildContext context) {
                return <PopupMenuEntry<String>>[
                  const PopupMenuItem<String>(
                    padding: EdgeInsets.zero,
                    value: '0',
                    child: Text('Item 0'),
                  ),
                  const PopupMenuItem<String>(
                    padding: EdgeInsets.zero,
                    height: 0,
                    value: '0',
                    child: Text('Item 1'),
                  ),
                  const PopupMenuItem<String>(
                    padding: EdgeInsets.all(20),
                    value: '0',
                    child: Text('Item 2'),
                  ),
                  const PopupMenuItem<String>(
                    padding: EdgeInsets.all(20),
                    height: 100,
                    value: '0',
                    child: Text('Item 3'),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Show the menu
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    // The menu items and their InkWells should have the expected vertical size
    // given the interactions between heights and padding.
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48); // Minimum interactive height (48)
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 16); // Height of text (16)
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 56); // Padding (20.0 + 20.0) + Height of text (16) = 56
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (16) + padding (40) < 100

    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero);
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero);
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20));
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20));
  });

  testWidgets('CheckedPopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupMenuButtonKey,
              child: const Text('button'),
              onSelected: (String result) { },
              itemBuilder: (BuildContext context) {
                return <PopupMenuEntry<String>>[
                  // This menu item's height will be 56.0 because the default minimum height
                  // is 48, but the contents of PopupMenuItem are 56.0 tall.
                  const CheckedPopupMenuItem<String>(
                    checked: true,
                    value: '0',
                    child: Text('Item 0'),
                  ),
                  // This menu item's height parameter specifies its minimum height. The
                  // overall height of the menu item will be 60 because the child's
                  // height 56, is less than 60.
                  const CheckedPopupMenuItem<String>(
                    checked: true,
                    height: 60,
                    value: '1',
                    child: SizedBox(
                      height: 40,
                      child: Text('Item 1'),
                    ),
                  ),
                  // This menu item's height parameter specifies its minimum height, so the
                  // overall height of the menu item will be 75.
                  const CheckedPopupMenuItem<String>(
                    checked: true,
                    height: 75,
                    value: '2',
                    child: SizedBox(
                      child: Text('Item 2'),
                    ),
                  ),
                  // This menu item's height will be 100.
                  const CheckedPopupMenuItem<String>(
                    checked: true,
                    height: 100,
                    value: '3',
                    child: SizedBox(
                      child: Text('Item 3'),
                    ),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Show the menu
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    // The menu items and their InkWells should have the expected vertical size
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 60);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 75);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100);
    // We evaluate the InkWell at the first index because that is the ListTile's
    // InkWell, which wins in the gesture arena over the child's InkWell and
    // is the one of interest.
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 0').at(1)).height, 56);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 1').at(1)).height, 60);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 2').at(1)).height, 75);
    expect(tester.getSize(find.widgetWithText(InkWell, 'Item 3').at(1)).height, 100);

    // Menu item children which whose height is less than the PopupMenuItem
    // are vertically centered.
    expect(
      tester.getRect(find.widgetWithText(menuItemType, 'Item 0')).center.dy,
      tester.getRect(find.text('Item 0')).center.dy,
    );
    expect(
      tester.getRect(find.widgetWithText(menuItemType, 'Item 2')).center.dy,
      tester.getRect(find.text('Item 2')).center.dy,
    );
  });

  testWidgets('CheckedPopupMenuItem custom padding', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const CheckedPopupMenuItem<String>(child: Text('item')).runtimeType;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupMenuButtonKey,
              child: const Text('button'),
              onSelected: (String result) { },
              itemBuilder: (BuildContext context) {
                return <PopupMenuEntry<String>>[
                  const CheckedPopupMenuItem<String>(
                    padding: EdgeInsets.zero,
                    value: '0',
                    child: Text('Item 0'),
                  ),
                  const CheckedPopupMenuItem<String>(
                    padding: EdgeInsets.zero,
                    height: 0,
                    value: '0',
                    child: Text('Item 1'),
                  ),
                  const CheckedPopupMenuItem<String>(
                    padding: EdgeInsets.all(20),
                    value: '0',
                    child: Text('Item 2'),
                  ),
                  const CheckedPopupMenuItem<String>(
                    padding: EdgeInsets.all(20),
                    height: 100,
                    value: '0',
                    child: Text('Item 3'),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Show the menu
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    // The menu items and their InkWells should have the expected vertical size
    // given the interactions between heights and padding.
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56); // Minimum ListTile height (56)
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 56); // Minimum ListTile height (56)
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 96); // Padding (20.0 + 20.0) + Height of ListTile (56) = 96
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (56) + padding (40) < 100

    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero);
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero);
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20));
    expect(tester.widget<Container>(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20));
  });

  testWidgets('Update PopupMenuItem layout while the menu is visible', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;

    Widget buildFrame({
      TextDirection textDirection = TextDirection.ltr,
      double fontSize = 24,
    }) {
      return MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return Directionality(
            textDirection: textDirection,
            child: PopupMenuTheme(
              data: PopupMenuTheme.of(context).copyWith(
                textStyle: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: fontSize),
              ),
              child: child!,
            ),
          );
        },
        home: Scaffold(
          body: PopupMenuButton<String>(
            key: popupMenuButtonKey,
            child: const Text('button'),
            onSelected: (String result) { },
            itemBuilder: (BuildContext context) {
              return <PopupMenuEntry<String>>[
                const PopupMenuItem<String>(
                  value: '0',
                  child: Text('Item 0'),
                ),
                const PopupMenuItem<String>(
                  value: '1',
                  child: Text('Item 1'),
                ),
              ];
            },
          ),
        ),
      );
    }

    // Show the menu
    await tester.pumpWidget(buildFrame());
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    // The menu items should have their default heights and horizontal alignment.
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 48);
    expect(tester.getTopLeft(find.text('Item 0')).dx, 24);
    expect(tester.getTopLeft(find.text('Item 1')).dx, 24);

    // While the menu is up, change its font size to 64 (default is 16).
    await tester.pumpWidget(buildFrame(fontSize: 64));
    await tester.pumpAndSettle(); // Theme changes are animated.
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 128);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 128);
    expect(tester.getSize(find.text('Item 0')).height, 128);
    expect(tester.getSize(find.text('Item 1')).height, 128);
    expect(tester.getTopLeft(find.text('Item 0')).dx, 24);
    expect(tester.getTopLeft(find.text('Item 1')).dx, 24);

    // While the menu is up, change the textDirection to rtl. Now menu items
    // will be aligned right.
    await tester.pumpWidget(buildFrame(textDirection: TextDirection.rtl));
    await tester.pumpAndSettle(); // Theme changes are animated.
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 48);
    expect(tester.getTopLeft(find.text('Item 0')).dx, 72);
    expect(tester.getTopLeft(find.text('Item 1')).dx, 72);
  });

  test("PopupMenuButton's child and icon properties cannot be simultaneously defined", () {
    expect(() {
      PopupMenuButton<int>(
        itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[],
        icon: const Icon(Icons.error),
        child: Container(),
      );
    }, throwsAssertionError);
  });

  testWidgets('PopupMenuButton default tooltip', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              // Default Tooltip should be present when [PopupMenuButton.icon]
              // and [PopupMenuButton.child] are undefined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
              ),
              // Default Tooltip should be present when
              // [PopupMenuButton.child] is defined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                child: const Text('Test text'),
              ),
              // Default Tooltip should be present when
              // [PopupMenuButton.icon] is defined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                icon: const Icon(Icons.check),
              ),
            ],
          ),
        ),
      ),
    );

    // The default tooltip is defined as [MaterialLocalizations.showMenuTooltip]
    // and it is used when no tooltip is provided.
    expect(find.byType(Tooltip), findsNWidgets(3));
    expect(find.byTooltip(const DefaultMaterialLocalizations().showMenuTooltip), findsNWidgets(3));
  });

  testWidgets('PopupMenuButton custom tooltip', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              // Tooltip should work when [PopupMenuButton.icon]
              // and [PopupMenuButton.child] are undefined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                tooltip: 'Test tooltip',
              ),
              // Tooltip should work when
              // [PopupMenuButton.child] is defined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                tooltip: 'Test tooltip',
                child: const Text('Test text'),
              ),
              // Tooltip should work when
              // [PopupMenuButton.icon] is defined.
              PopupMenuButton<int>(
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
                tooltip: 'Test tooltip',
                icon: const Icon(Icons.check),
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.byType(Tooltip), findsNWidgets(3));
    expect(find.byTooltip('Test tooltip'), findsNWidgets(3));
  });

  testWidgets('Allow Widget for PopupMenuButton.icon', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: PopupMenuButton<int>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuEntry<int>>[
                const PopupMenuItem<int>(
                  value: 1,
                  child: Text('Tap me please!'),
                ),
              ];
            },
            tooltip: 'Test tooltip',
            icon: const Text('PopupMenuButton icon'),
          ),
        ),
      ),
    );

    expect(find.text('PopupMenuButton icon'), findsOneWidget);
  });

  testWidgets('showMenu uses nested navigator by default', (WidgetTester tester) async {
    final MenuObserver rootObserver = MenuObserver();
    final MenuObserver nestedObserver = MenuObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showMenu<int>(
                    context: context,
                    position: RelativeRect.fill,
                    items: <PopupMenuItem<int>>[
                      const PopupMenuItem<int>(
                        value: 1, child: Text('1'),
                      ),
                    ],
                  );
                },
                child: const Text('Show Menu'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(ElevatedButton));

    expect(rootObserver.menuCount, 0);
    expect(nestedObserver.menuCount, 1);
  });

  testWidgets('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async {
    final MenuObserver rootObserver = MenuObserver();
    final MenuObserver nestedObserver = MenuObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showMenu<int>(
                    context: context,
                    useRootNavigator: true,
                    position: RelativeRect.fill,
                    items: <PopupMenuItem<int>>[
                      const PopupMenuItem<int>(
                        value: 1, child: Text('1'),
                      ),
                    ],
                  );
                },
                child: const Text('Show Menu'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(ElevatedButton));

    expect(rootObserver.menuCount, 1);
    expect(nestedObserver.menuCount, 0);
  });

  testWidgets('PopupMenuButton calling showButtonMenu manually', (WidgetTester tester) async {
    final GlobalKey<PopupMenuButtonState<int>> globalKey = GlobalKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              PopupMenuButton<int>(
                key: globalKey,
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(
                      value: 1,
                      child: Text('Tap me please!'),
                    ),
                  ];
                },
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.text('Tap me please!'), findsNothing);

    globalKey.currentState!.showButtonMenu();
    // The PopupMenuItem will appear after an animation, hence,
    // we have to first wait for the tester to settle.
    await tester.pumpAndSettle();

    expect(find.text('Tap me please!'), findsOneWidget);
  });

  testWidgets('PopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async {
    const Key key = ValueKey<int>(1);
    // Test PopupMenuItem() constructor
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: PopupMenuItem<int>(
                  key: key,
                  mouseCursor: SystemMouseCursors.text,
                  value: 1,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: tester.getCenter(find.byKey(key)));

    await tester.pump();

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);

    // Test default cursor
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: PopupMenuItem<int>(
                  key: key,
                  value: 1,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);

    // Test default cursor when disabled
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: PopupMenuItem<int>(
                  key: key,
                  value: 1,
                  enabled: false,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
  });

  testWidgets('CheckedPopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async {
    const Key key = ValueKey<int>(1);
    // Test CheckedPopupMenuItem() constructor
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: CheckedPopupMenuItem<int>(
                  key: key,
                  mouseCursor: SystemMouseCursors.text,
                  value: 1,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
    addTearDown(gesture.removePointer);

    await tester.pump();

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);

    // Test default cursor
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: CheckedPopupMenuItem<int>(
                  key: key,
                  value: 1,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);

    // Test default cursor when disabled
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: CheckedPopupMenuItem<int>(
                  key: key,
                  value: 1,
                  enabled: false,
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
  });

  testWidgets('PopupMenu in AppBar does not overlap with the status bar', (WidgetTester tester) async {
    const List<PopupMenuItem<int>> choices = <PopupMenuItem<int>>[
      PopupMenuItem<int>(value: 1, child: Text('Item 1')),
      PopupMenuItem<int>(value: 2, child: Text('Item 2')),
      PopupMenuItem<int>(value: 3, child: Text('Item 3')),
    ];

    const double statusBarHeight = 24.0;
    final PopupMenuItem<int> firstItem = choices[0];
    int selectedValue = choices[0].value!;

    await tester.pumpWidget(
      MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            data: const MediaQueryData(padding: EdgeInsets.only(top: statusBarHeight)), // status bar
            child: child!,
          );
        },
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('PopupMenu Test'),
                actions: <Widget>[
                  PopupMenuButton<int>(
                    onSelected: (int result) {
                      setState(() {
                        selectedValue = result;
                      });
                    },
                    initialValue: selectedValue,
                    itemBuilder: (BuildContext context) {
                      return choices;
                    },
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );

    await tester.tap(find.byIcon(Icons.more_vert));
    await tester.pumpAndSettle();

    // Tap third item.
    await tester.tap(find.text('Item 3'));
    await tester.pumpAndSettle();

    // Open popupMenu again.
    await tester.tap(find.byIcon(Icons.more_vert));
    await tester.pumpAndSettle();

    // Check whether the first item is not overlapping with status bar.
    expect(tester.getTopLeft(find.byWidget(firstItem)).dy, greaterThan(statusBarHeight));
  });

  testWidgets('Vertically long PopupMenu does not overlap with the status bar and bottom notch', (WidgetTester tester) async {
    const double windowPaddingTop = 44;
    const double windowPaddingBottom = 34;

    await tester.pumpWidget(
      MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            data: const MediaQueryData(
              padding: EdgeInsets.only(
                top: windowPaddingTop,
                bottom: windowPaddingBottom,
              ),
            ),
            child: child!,
          );
        },
        home: Scaffold(
          appBar: AppBar(
            title: const Text('PopupMenu Test'),
          ),
          body: PopupMenuButton<int>(
            child: const Text('Show Menu'),
            itemBuilder: (BuildContext context) => Iterable<PopupMenuItem<int>>.generate(
              20, (int i) => PopupMenuItem<int>(
                value: i,
                child: Text('Item $i'),
              ),
            ).toList(),
          ),
        ),
      ),
    );

    await tester.tap(find.text('Show Menu'));
    await tester.pumpAndSettle();

    final Offset topRightOfMenu = tester.getTopRight(find.byType(SingleChildScrollView));
    final Offset bottomRightOfMenu = tester.getBottomRight(find.byType(SingleChildScrollView));

    expect(topRightOfMenu.dy, windowPaddingTop + 8.0);
    expect(bottomRightOfMenu.dy, 600.0 - windowPaddingBottom - 8.0); // Screen height is 600.
  });

  testWidgets('PopupMenu position test when have unsafe area', (WidgetTester tester) async {
    final GlobalKey buttonKey = GlobalKey();

    Widget buildFrame(double width, double height) {
      return MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            data: const MediaQueryData(
              padding: EdgeInsets.only(
                top: 32.0,
                bottom: 32.0,
              ),
            ),
            child: child!,
          );
        },
        home: Scaffold(
          appBar: AppBar(
            title: const Text('PopupMenu Test'),
            actions: <Widget>[
              PopupMenuButton<int>(
                child: SizedBox(
                  key: buttonKey,
                  height: height,
                  width: width,
                  child: const ColoredBox(
                    color: Colors.pink,
                  ),
                ),
                itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
                  const PopupMenuItem<int>(value: 1, child: Text('-1-')),
                  const PopupMenuItem<int>(value: 2, child: Text('-2-')),
                ],
              ),
            ],
          ),
          body: Container(),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(20.0, 20.0));

    await tester.tap(find.byKey(buttonKey));
    await tester.pumpAndSettle();

    final Offset button = tester.getTopRight(find.byKey(buttonKey));
    expect(button, const Offset(800.0, 32.0)); // The topPadding is 32.0.

    final Offset popupMenu = tester.getTopRight(find.byType(SingleChildScrollView));

    // The menu should be positioned directly next to the top of the button.
    // The 8.0 pixels is [_kMenuScreenPadding].
    expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0));
  });

  // Regression test for https://github.com/flutter/flutter/issues/82874
  testWidgets('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async {
    final GlobalKey buttonKey = GlobalKey();
    const EdgeInsets padding = EdgeInsets.only(left: 300.0, top: 32.0, right: 310.0, bottom: 64.0);
    EdgeInsets? mediaQueryPadding;

    Widget buildFrame(double width, double height) {
      return MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            data: const MediaQueryData(
              padding: padding,
            ),
            child: child!,
          );
        },
        home: Scaffold(
          appBar: AppBar(
            title: const Text('PopupMenu Test'),
            actions: <Widget>[
              PopupMenuButton<int>(
                child: SizedBox(
                  key: buttonKey,
                  height: height,
                  width: width,
                  child: const ColoredBox(
                    color: Colors.pink,
                  ),
                ),
                itemBuilder: (BuildContext context) {
                  return <PopupMenuEntry<int>>[
                    PopupMenuItem<int>(
                      value: 1,
                      child: Builder(
                        builder: (BuildContext context) {
                          mediaQueryPadding = MediaQuery.paddingOf(context);
                          return Text('-1-' * 500); // A long long text string.
                        },
                      ),
                    ),
                    const PopupMenuItem<int>(value: 2, child: Text('-2-')),
                  ];
                },
              ),
            ],
          ),
          body: const SizedBox.shrink(),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(20.0, 20.0));

    await tester.tap(find.byKey(buttonKey));
    await tester.pumpAndSettle();

    final Offset button = tester.getTopRight(find.byKey(buttonKey));
    expect(button, Offset(800.0 - padding.right, padding.top)); // The topPadding is 32.0.

    final Offset popupMenuTopRight = tester.getTopRight(find.byType(SingleChildScrollView));

    // The menu should be positioned directly next to the top of the button.
    // The 8.0 pixels is [_kMenuScreenPadding].
    expect(popupMenuTopRight, Offset(800.0 - padding.right - 8.0, padding.top + 8.0));

    final Offset popupMenuTopLeft = tester.getTopLeft(find.byType(SingleChildScrollView));
    expect(popupMenuTopLeft, Offset(padding.left + 8.0, padding.top + 8.0));

    final Offset popupMenuBottomLeft = tester.getBottomLeft(find.byType(SingleChildScrollView));
    expect(popupMenuBottomLeft, Offset(padding.left + 8.0, 600.0 - padding.bottom - 8.0));

    // The `MediaQueryData.padding` should be removed.
    expect(mediaQueryPadding, EdgeInsets.zero);
  });

  group('feedback', () {
    late FeedbackTester feedback;

    setUp(() {
      feedback = FeedbackTester();
    });

    tearDown(() {
      feedback.dispose();
    });

    Widget buildFrame({ bool? widgetEnableFeedback, bool? themeEnableFeedback }) {
      return MaterialApp(
        home: Scaffold(
          body: PopupMenuTheme(
            data: PopupMenuThemeData(
              enableFeedback: themeEnableFeedback,
            ),
            child: PopupMenuButton<int>(
              enableFeedback: widgetEnableFeedback,
              child: const Text('Show Menu'),
              itemBuilder: (BuildContext context) {
                return <PopupMenuItem<int>>[
                  const PopupMenuItem<int>(
                    value: 1,
                    child: Text('One'),
                  ),
                ];
              },
            ),
          ),
        ),
      );
    }

    testWidgets('PopupMenuButton enableFeedback works properly', (WidgetTester tester) async {
      expect(feedback.clickSoundCount, 0);
      expect(feedback.hapticCount, 0);

      // PopupMenuButton with enabled feedback.
      await tester.pumpWidget(buildFrame(widgetEnableFeedback: true));
      await tester.tap(find.text('Show Menu'));
      await tester.pumpAndSettle();
      expect(feedback.clickSoundCount, 1);
      expect(feedback.hapticCount, 0);

      await tester.pumpWidget(Container());

      // PopupMenuButton with disabled feedback.
      await tester.pumpWidget(buildFrame(widgetEnableFeedback: false));
      await tester.tap(find.text('Show Menu'));
      await tester.pumpAndSettle();
      expect(feedback.clickSoundCount, 1);
      expect(feedback.hapticCount, 0);

      await tester.pumpWidget(Container());

      // PopupMenuButton with enabled feedback by default.
      await tester.pumpWidget(buildFrame());
      await tester.tap(find.text('Show Menu'));
      await tester.pumpAndSettle();
      expect(feedback.clickSoundCount, 2);
      expect(feedback.hapticCount, 0);

      await tester.pumpWidget(Container());

      // PopupMenu with disabled feedback using PopupMenuButtonTheme.
      await tester.pumpWidget(buildFrame(themeEnableFeedback: false));
      await tester.tap(find.text('Show Menu'));
      await tester.pumpAndSettle();
      expect(feedback.clickSoundCount, 2);
      expect(feedback.hapticCount, 0);

      await tester.pumpWidget(Container());

      // PopupMenu enableFeedback property overrides PopupMenuButtonTheme.
      await tester.pumpWidget(buildFrame(widgetEnableFeedback: false,themeEnableFeedback: true));
      await tester.tap(find.text('Show Menu'));
      await tester.pumpAndSettle();
      expect(feedback.clickSoundCount, 2);
      expect(feedback.hapticCount, 0);
    });
  });

  testWidgets('iconSize parameter tests', (WidgetTester tester) async {
    Future<void> buildFrame({double? iconSize}) {
      return tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: PopupMenuButton<String>(
                iconSize: iconSize,
                itemBuilder: (_) => <PopupMenuEntry<String>>[
                  const PopupMenuItem<String>(
                    value: 'value',
                    child: Text('child'),
                  ),
                ],
              ),
            ),
          ),
        ),
      );
    }

    await buildFrame();
    expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(24, 24));

    await buildFrame(iconSize: 50);
    expect(tester.getSize(find.byIcon(Icons.adaptive.more)), const Size(50, 50));
  });

  testWidgets('does not crash in small overlay', (WidgetTester tester) async {
    final GlobalKey navigator = GlobalKey();
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Column(
            children: <Widget>[
              OutlinedButton(
                onPressed: () {
                  showMenu<void>(
                    context: navigator.currentContext!,
                    position: RelativeRect.fill,
                    items: const <PopupMenuItem<void>>[
                      PopupMenuItem<void>(child: Text('foo')),
                    ],
                  );
                },
                child: const Text('press'),
              ),
              SizedBox(
                height: 10,
                width: 10,
                child: Navigator(
                  key: navigator,
                  onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(
                    builder: (BuildContext context) => Container(color: Colors.red),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );

    await tester.tap(find.text('press'));
    await tester.pumpAndSettle();
    expect(find.text('foo'), findsOneWidget);
  });

  // Regression test for https://github.com/flutter/flutter/issues/80869
  testWidgets('The menu position test in the scrollable widget', (WidgetTester tester) async {
    final GlobalKey buttonKey = GlobalKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                const SizedBox(height: 100),
                PopupMenuButton<int>(
                  child: SizedBox(
                    key: buttonKey,
                    height: 10.0,
                    width: 10.0,
                    child: const ColoredBox(
                      color: Colors.pink,
                    ),
                  ),
                  itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
                    const PopupMenuItem<int>(value: 1, child: Text('-1-')),
                    const PopupMenuItem<int>(value: 2, child: Text('-2-')),
                  ],
                ),
                const SizedBox(height: 600),
              ],
            ),
          ),
        ),
      ),
    );

    // Open the menu.
    await tester.tap(find.byKey(buttonKey));
    await tester.pumpAndSettle();

    Offset button = tester.getTopLeft(find.byKey(buttonKey));
    expect(button, const Offset(0.0, 100.0));

    Offset popupMenu = tester.getTopLeft(find.byType(SingleChildScrollView).last);
    // The menu should be positioned directly next to the top of the button.
    // The 8.0 pixels is [_kMenuScreenPadding].
    expect(popupMenu, const Offset(8.0, 100.0));

    // Close the menu.
    await tester.tap(find.byKey(buttonKey), warnIfMissed: false);
    await tester.pumpAndSettle();

    // Scroll a little bit.
    await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -50.0));

    button = tester.getTopLeft(find.byKey(buttonKey));
    expect(button, const Offset(0.0, 50.0));

    // Open the menu again.
    await tester.tap(find.byKey(buttonKey));
    await tester.pumpAndSettle();

    popupMenu = tester.getTopLeft(find.byType(SingleChildScrollView).last);
    // The menu should be positioned directly next to the top of the button.
    // The 8.0 pixels is [_kMenuScreenPadding].
    expect(popupMenu, const Offset(8.0, 50.0));
  });

  testWidgets('PopupMenuButton custom splash radius', (WidgetTester tester) async {
    Future<void> buildFrameWithoutChild({double? splashRadius}) {
      return tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: PopupMenuButton<String>(
                splashRadius: splashRadius,
                itemBuilder: (_) => <PopupMenuEntry<String>>[
                  const PopupMenuItem<String>(
                    value: 'value',
                    child: Text('child'),
                  ),
                ],
              ),
            ),
          ),
        ),
      );
    }

    Future<void> buildFrameWithChild({double? splashRadius}) {
      return tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: PopupMenuButton<String>(
                splashRadius: splashRadius,
                child: const Text('An item'),
                itemBuilder: (_) => <PopupMenuEntry<String>>[
                  const PopupMenuDivider(),
                ],
              ),
            ),
          ),
        ),
      );
    }


    await buildFrameWithoutChild();
    expect(tester.widget<InkResponse>(find.byType(InkResponse)).radius,
        Material.defaultSplashRadius);
    await buildFrameWithChild();
    expect(tester.widget<InkWell>(find.byType(InkWell)).radius, null);


    const double testSplashRadius = 50;

    await buildFrameWithoutChild(splashRadius: testSplashRadius);
    expect(tester.widget<InkResponse>(find.byType(InkResponse)).radius,
        testSplashRadius);
    await buildFrameWithChild(splashRadius: testSplashRadius);
    expect(tester.widget<InkWell>(find.byType(InkWell)).radius,
        testSplashRadius);
  });

  testWidgets('Can override menu size constraints', (WidgetTester tester) async {
    final Key popupMenuButtonKey = UniqueKey();
    final Type menuItemType = const PopupMenuItem<String>(child: Text('item')).runtimeType;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupMenuButtonKey,
              constraints: const BoxConstraints(
                minWidth: 500,
              ),
              itemBuilder: (_) => <PopupMenuEntry<String>>[
                const PopupMenuItem<String>(
                  value: 'value',
                  child: Text('Item 0'),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    // Show the menu
    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pumpAndSettle();

    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
    expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).width, 500);
  });

  testWidgets('Can change menu position and offset', (WidgetTester tester) async {
    PopupMenuButton<int> buildMenuButton({required PopupMenuPosition position}) {
      return PopupMenuButton<int>(
        position: position,
        itemBuilder: (BuildContext context) {
          return <PopupMenuItem<int>>[
            PopupMenuItem<int>(
              value: 1,
              child: Builder(
                builder: (BuildContext context) {
                  return const Text('AAA');
                },
              ),
            ),
          ];
        },
      );
    }

    // Popup menu with `MenuPosition.over (default) with default offset`.
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: buildMenuButton(position: PopupMenuPosition.over),
          ),
        ),
      ),
    );

    // Open the popup menu.
    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));

    // Close the popup menu.
    await tester.tapAt(Offset.zero);
    await tester.pump();

    // Popup menu with `MenuPosition.under`(custom) with default offset`.
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: buildMenuButton(position: PopupMenuPosition.under),
          ),
        ),
      ),
    );

    // Open the popup menu.
    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 40.0));

    // Close the popup menu.
    await tester.tapAt(Offset.zero);
    await tester.pump();

    // Popup menu with `MenuPosition.over (default) with custom offset`.
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: PopupMenuButton<int>(
              offset: const Offset(0.0, 50),
              itemBuilder: (BuildContext context) {
                return <PopupMenuItem<int>>[
                  PopupMenuItem<int>(
                    value: 1,
                    child: Builder(
                      builder: (BuildContext context) {
                        return const Text('AAA');
                      },
                    ),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Open the popup menu.
    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 50.0));

    // Close the popup menu.
    await tester.tapAt(Offset.zero);
    await tester.pump();

    // Popup menu with `MenuPosition.under (custom) with custom offset`.
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Material(
            child: PopupMenuButton<int>(
              offset: const Offset(0.0, 50),
              position: PopupMenuPosition.under,
              itemBuilder: (BuildContext context) {
                return <PopupMenuItem<int>>[
                  PopupMenuItem<int>(
                    value: 1,
                    child: Builder(
                      builder: (BuildContext context) {
                        return const Text('AAA');
                      },
                    ),
                  ),
                ];
              },
            ),
          ),
        ),
      ),
    );

    // Open the popup menu.
    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 90.0));
  });

  testWidgets("PopupMenuButton icon inherits IconTheme's size", (WidgetTester tester) async {
    Widget buildPopupMenu({double? themeIconSize, double? iconSize}) {
      return MaterialApp(
        theme: ThemeData(
          iconTheme: IconThemeData(
            size: themeIconSize,
          ),
        ),
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              iconSize: iconSize,
              itemBuilder: (_) => <PopupMenuEntry<String>>[
                const PopupMenuItem<String>(
                  value: 'value',
                  child: Text('Item 0'),
                ),
              ],
            ),
          ),
        ),
      );
    }

    // Popup menu with default icon size.
    await tester.pumpWidget(buildPopupMenu());
    // Default PopupMenuButton icon size is 24.0.
    expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(24.0, 24.0));

    // Popup menu with custom theme icon size.
    await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0));
    await tester.pumpAndSettle();
    // PopupMenuButton icon inherits IconTheme's size.
    expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(30.0, 30.0));

    // Popup menu with custom icon size.
    await tester.pumpWidget(buildPopupMenu(themeIconSize: 30.0, iconSize: 50.0));
    await tester.pumpAndSettle();
    // PopupMenuButton icon size overrides IconTheme's size.
    expect(tester.getSize(find.byIcon(Icons.more_vert)), const Size(50.0, 50.0));
  });

  testWidgets('Popup menu clip behavior', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/107215
    final Key popupButtonKey = UniqueKey();
    const double radius = 20.0;

    Widget buildPopupMenu({required Clip clipBehavior}) {
      return MaterialApp(
        home: Scaffold(
          body: Center(
            child: PopupMenuButton<String>(
              key: popupButtonKey,
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(radius)),
              ),
              clipBehavior: clipBehavior,
              itemBuilder: (_) => <PopupMenuEntry<String>>[
                const PopupMenuItem<String>(
                  value: 'value',
                  child: Text('Item 0'),
                ),
              ],
            ),
          ),
        ),
      );
    }

    // Popup menu with default ClipBehavior.
    await tester.pumpWidget(buildPopupMenu(clipBehavior: Clip.none));

    // Open the popup to build and show the menu contents.
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();

    Material material = tester.widget<Material>(find.byType(Material).last);
    expect(material.clipBehavior, Clip.none);

    // Close the popup menu.
    await tester.tapAt(Offset.zero);
    await tester.pumpAndSettle();

    // Popup menu with custom ClipBehavior.
    await tester.pumpWidget(buildPopupMenu(clipBehavior: Clip.hardEdge));

    // Open the popup to build and show the menu contents.
    await tester.tap(find.byKey(popupButtonKey));
    await tester.pumpAndSettle();

    material = tester.widget<Material>(find.byType(Material).last);
    expect(material.clipBehavior, Clip.hardEdge);
  });
}

class TestApp extends StatelessWidget {
  const TestApp({
    super.key,
    required this.textDirection,
    this.child,
  });

  final TextDirection textDirection;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return Localizations(
      locale: const Locale('en', 'US'),
      delegates: const <LocalizationsDelegate<dynamic>>[
        DefaultWidgetsLocalizations.delegate,
        DefaultMaterialLocalizations.delegate,
      ],
      child: MediaQuery(
        data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
        child: Directionality(
          textDirection: textDirection,
          child: Navigator(
            onGenerateRoute: (RouteSettings settings) {
              assert(settings.name == '/');
              return MaterialPageRoute<void>(
                settings: settings,
                builder: (BuildContext context) => Material(
                  child: child,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class MenuObserver extends NavigatorObserver {
  int menuCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route.toString().contains('_PopupMenuRoute')) {
      menuCount++;
    }
    super.didPush(route, previousRoute);
  }
}
